Skip to main content

rakata_formats/gff/
document.rs

1//! An open GFF file, editable by path and readable as any typed view.
2//!
3//! # Why it holds the bytes it was opened from
4//!
5//! Re-serialising a parsed tree loses nothing and moves plenty. Measured over
6//! every GFF in a vanilla install, the writer reproduces some files byte for
7//! byte and re-lays out the rest, with every one of the rearranged files
8//! re-reading to a tree equal to the original. Measured over a folder of
9//! saves, it reproduces none of them. The rearranging is ordering inside a
10//! block rather than a change of content: only a handful of install files come
11//! out with a differently-sized block at all.
12//!
13//! So a document that parsed on open and serialised on save would rewrite most
14//! files it was merely asked to look at. This one keeps the bytes it was handed
15//! and gives them back untouched until something actually changes, which makes
16//! open-then-save a no-op by construction rather than by the writer and the
17//! toolset happening to agree.
18//!
19//! # What survives an edit
20//!
21//! Everything. The tree is the whole file, so a label no view models is still
22//! there after [`set`](GffDocument::set) has changed one that is. That is the
23//! difference between this and reading a typed view, editing it and authoring
24//! it back: a view models only what the engine reads, so authoring from one
25//! drops the rest.
26//!
27//! # A closed set of mutators, and no way past them
28//!
29//! [`set`](GffDocument::set), [`insert`](GffDocument::insert),
30//! [`remove`](GffDocument::remove),
31//! [`push_element`](GffDocument::push_element) and
32//! [`set_struct_id_at`](GffDocument::set_struct_id_at) are the whole surface.
33//! There is no `&mut Gff`, and that narrowing is what makes this document able
34//! to say whether it has been edited at all: [`gff`](GffDocument::gff) lends
35//! the tree out for reading and [`view`](GffDocument::view) hands back an owned
36//! projection, so nothing outside can change the tree without going through one
37//! of them.
38//!
39//! # The `_at` forms are the primary ones
40//!
41//! Every operation comes in two spellings: one taking `&str` in the dot
42//! notation `ClassList[0].Class`, and an `_at` one taking a [`GffPath`]. The
43//! text forms came first and read as the real API, and they are not. Each
44//! parses its argument and calls the `_at` form, so the address is what this
45//! document actually works in and the text is a convenience over it.
46//!
47//! Which matters at a call site holding a path already. A raw tree keeps a
48//! [`GffPath`] per row and a change map is keyed by one, so reading through the
49//! text form would render an address to a `String` and parse it back to reach
50//! the value it was already pointing at. Reach for `_at` unless the address
51//! genuinely arrived as text, from a config file, a lint rule or a user.
52//!
53//! Their error types differ for the same reason. The text forms answer
54//! [`GffDocumentError`], which has a syntax arm; the `_at` forms answer
55//! [`GffPathError`], because an address that already parsed cannot fail to.
56//!
57//! # No schema is consulted
58//!
59//! [`set`](GffDocument::set) takes whatever the caller's Rust type maps to and
60//! [`GffStruct::set`] refuses it if the stored field is a different width. That
61//! refusal is the whole check. Widening the field to fit would shift every byte
62//! after it, and looking the label up in a schema would mean the document
63//! deciding which format it thinks the file is, which it cannot do for a mod's
64//! own label and should not do for anyone's.
65
66use std::collections::BTreeMap;
67use std::str::FromStr;
68
69use thiserror::Error;
70
71use super::{read_gff_from_bytes, write_gff_to_vec, Gff, GffBinaryError, GffStruct, GffValue};
72use super::{GffPath, GffPathError, GffPathParseError, GffPathSegment};
73use crate::schema::{FromGff, GffScalar};
74
75/// A path that does not parse, or does not name one field of this tree.
76#[derive(Debug, Clone, PartialEq, Eq, Error)]
77pub enum GffDocumentError {
78    /// The path text is not an address.
79    #[error(transparent)]
80    Syntax(#[from] GffPathParseError),
81    /// The address does not resolve, or the tree will not take the edit.
82    #[error(transparent)]
83    Path(#[from] GffPathError),
84    /// The field is there and holds something the caller cannot read it as.
85    ///
86    /// Separate from the address not resolving, because those want different
87    /// answers: a missing field is often ordinary and a field of the wrong
88    /// width is a file that is not what it claimed to be.
89    #[error("`{at}` does not hold a {wanted}")]
90    WrongType {
91        /// The address that resolved.
92        at: String,
93        /// The type the caller asked for.
94        wanted: &'static str,
95    },
96}
97
98/// What happened at one address, from open until now.
99///
100/// `before` is what was there when the document was opened, not what was there
101/// last time. An editor's review pane shows a user what saving would do to the
102/// file they opened, and a chain of intermediate values is not that.
103///
104/// `None` on either side means the address held nothing: absent before an
105/// insert, absent after a remove.
106#[derive(Debug, Clone, PartialEq)]
107pub struct Change {
108    /// What the opened file held there.
109    pub before: Option<GffValue>,
110    /// What it holds there now.
111    pub current: Option<GffValue>,
112}
113
114/// What a mutation does to whatever is already recorded beneath its address.
115///
116/// The mutator's own statement, because the address does not carry it: a
117/// removal and an append at one list address are both mutations of the list
118/// and only the first makes a change recorded inside it obsolete.
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120enum Subtree {
121    /// What was under the address is gone or replaced, so anything recorded
122    /// under it no longer describes this file.
123    Replaced,
124    /// What was under the address is still there, untouched and still at the
125    /// same addresses.
126    Kept,
127}
128
129/// One open GFF file: its bytes, its tree, and what has happened to it.
130#[derive(Debug, Clone, PartialEq)]
131pub struct GffDocument {
132    source: Vec<u8>,
133    gff: Gff,
134    changes: BTreeMap<GffPath, Change>,
135    revision: u64,
136}
137
138impl GffDocument {
139    /// Parses `source` and keeps a copy of it.
140    ///
141    /// # Errors
142    ///
143    /// Returns [`GffBinaryError`] when the bytes are not a GFF this reader
144    /// accepts.
145    pub fn open(source: &[u8]) -> Result<Self, GffBinaryError> {
146        Ok(Self {
147            gff: read_gff_from_bytes(source)?,
148            source: source.to_vec(),
149            changes: BTreeMap::new(),
150            revision: 0,
151        })
152    }
153
154    /// How many times this document has been mutated.
155    ///
156    /// Zero on open, rising by one per mutation that the tree accepted. A
157    /// mutation returning a value to what the file held records no change and
158    /// still counts, so this moves when [`changes`](Self::changes) does not.
159    ///
160    /// For a consumer caching state derived from the tree.
161    pub fn revision(&self) -> u64 {
162        self.revision
163    }
164
165    /// The parsed tree, including everything no view models.
166    pub fn gff(&self) -> &Gff {
167        &self.gff
168    }
169
170    /// Whether the tree differs from the file this was opened from.
171    ///
172    /// Whether the content differs, not whether a mutator ran. Setting a field
173    /// to the value it already held leaves this `false`, which matters because
174    /// a UI binding a text field writes the unchanged value on every redraw and
175    /// [`save`](Self::save) re-serialises whenever this is `true`.
176    pub fn is_edited(&self) -> bool {
177        !self.changes.is_empty()
178    }
179
180    /// Every address whose content differs from the opened file, in path
181    /// order.
182    ///
183    /// Recorded as the edits are made rather than derived by comparing trees.
184    /// This document is the only thing that can change its own tree, so a map
185    /// it keeps cannot fall behind: [`gff`](Self::gff) lends the tree for
186    /// reading, [`view`](Self::view) hands back an owned projection, and the
187    /// four mutators all funnel through one recording point. The drift that
188    /// makes an event log untrustworthy is a hazard for something *observing*
189    /// mutations, not for the one making them.
190    ///
191    /// An address that has come back to what it held is not in here. Setting a
192    /// field and setting it back leaves nothing, appending an element and
193    /// removing it again leaves nothing, and setting one field twice leaves one
194    /// entry against the value the file was opened with.
195    pub fn changes(&self) -> &BTreeMap<GffPath, Change> {
196        &self.changes
197    }
198
199    /// Reads this file as a typed view.
200    ///
201    /// The view is a projection and reading one changes nothing here, so a
202    /// caller can take several views of the same document and still save the
203    /// file it opened.
204    ///
205    /// # Errors
206    ///
207    /// Whatever the view's own reader reports.
208    pub fn view<T: FromGff>(&self) -> Result<T, T::Error> {
209        T::from_gff(&self.gff)
210    }
211
212    /// Reads the value at `path`, in the dot notation `ClassList[0].Class`.
213    ///
214    /// Parses the text and delegates to [`get_at`](Self::get_at). A caller that
215    /// already holds a [`GffPath`] wants that one instead: this renders nothing
216    /// and allocates nothing, but it does have to parse, and a caller looping
217    /// over addresses it already has would be paying for a round trip through
218    /// text it never needed.
219    ///
220    /// # Errors
221    ///
222    /// [`GffDocumentError::Syntax`] when the text is not an address, and
223    /// [`GffDocumentError::Path`] when it does not name exactly one field of
224    /// this tree.
225    pub fn get(&self, path: &str) -> Result<&GffValue, GffDocumentError> {
226        let path = GffPath::from_str(path)?;
227        Ok(self.get_at(&path)?)
228    }
229
230    /// Reads the value `at` names.
231    ///
232    /// # Errors
233    ///
234    /// [`GffPathError`] when the address does not name exactly one field of
235    /// this tree. There is no syntax error to report, which is the whole
236    /// difference between this family and the text one.
237    pub fn get_at(&self, at: &GffPath) -> Result<&GffValue, GffPathError> {
238        self.gff.root.get(at)
239    }
240
241    /// The struct id of the list element `at` names.
242    ///
243    /// The reader half of [`set_struct_id_at`](Self::set_struct_id_at), and the
244    /// only way to see an id without reaching past this document into the tree.
245    /// Several formats put meaning there and in no field: a UTC's equipment
246    /// slot **is** its `Equip_ItemList` element's id, so reading which slot an
247    /// item occupies is exactly this call.
248    ///
249    /// # Errors
250    ///
251    /// [`GffPathError`] when `at` does not end in a list index, or indexes past
252    /// the end of the list.
253    pub fn struct_id_at(&self, at: &GffPath) -> Result<i32, GffPathError> {
254        self.gff.root.struct_id(at)
255    }
256
257    /// Reads the value at `path` as `T`.
258    ///
259    /// The typed counterpart to [`get`](Self::get), which hands back the wire
260    /// value and leaves the caller to match on its variant. Every field a
261    /// panel binds a control to goes through that unwrap, so it is written
262    /// here once instead of at each of them.
263    ///
264    /// Accepts every encoding the type can be read from, which is
265    /// [`GffScalar`]'s own tolerance rather than a second rule: a field
266    /// declared narrower than the caller's type still reads.
267    ///
268    /// # Errors
269    ///
270    /// [`GffDocumentError::Syntax`] when the text is not an address,
271    /// [`GffDocumentError::Path`] when it names no single field, and
272    /// [`GffDocumentError::WrongType`] when the field is there and holds
273    /// something else. The last is kept apart from the second because a field
274    /// that is absent and a field that is the wrong shape are different facts
275    /// about the file.
276    pub fn get_as<T: GffScalar>(&self, path: &str) -> Result<T, GffDocumentError> {
277        let value = self.get(path)?;
278        T::from_gff_value(value).ok_or_else(|| GffDocumentError::WrongType {
279            at: path.to_owned(),
280            wanted: T::GFF_TYPE.name(),
281        })
282    }
283
284    /// Replaces the value at `path`, returning what was there.
285    ///
286    /// The field has to be present already and the replacement has to be the
287    /// width the field stores. Both refusals are [`GffStruct::set`]'s, and its
288    /// docs say why.
289    ///
290    /// [`GffStruct::set`]: super::GffStruct::set
291    ///
292    /// # Errors
293    ///
294    /// [`GffDocumentError::Syntax`] when the text is not an address, and
295    /// [`GffDocumentError::Path`] when the field is absent, is carried more
296    /// than once, or stores a different width.
297    pub fn set(&mut self, path: &str, value: impl GffScalar) -> Result<GffValue, GffDocumentError> {
298        let path = GffPath::from_str(path)?;
299        Ok(self.set_at(&path, value.to_gff_value())?)
300    }
301
302    /// Adds the field `path` names, which must not already be there.
303    ///
304    /// The value's own type decides the field's type: there is nothing stored
305    /// to preserve, which is why this takes what it is given where
306    /// [`set`](Self::set) refuses a width change.
307    ///
308    /// Scalars only, and that is this spelling's limit rather than the
309    /// document's: [`insert_at`](Self::insert_at) takes a [`GffValue`], so a
310    /// caller adding a list or a nested struct goes through that one. A raw
311    /// tree offering "add a field" does exactly that.
312    ///
313    /// # Errors
314    ///
315    /// [`GffDocumentError::Syntax`] when the text is not an address, and
316    /// [`GffDocumentError::Path`] when the parent does not resolve or the
317    /// label is already there.
318    pub fn insert(&mut self, path: &str, value: impl GffScalar) -> Result<(), GffDocumentError> {
319        let path = GffPath::from_str(path)?;
320        Ok(self.insert_at(&path, value.to_gff_value())?)
321    }
322
323    /// Removes what `path` names, returning it.
324    ///
325    /// A path ending in a label removes that field; one ending in an index
326    /// removes that list element and hands it back wrapped in a
327    /// [`GffValue::Struct`]. Siblings are not renumbered, for the reason
328    /// [`GffStruct::remove`] gives.
329    ///
330    /// [`GffStruct::remove`]: super::GffStruct::remove
331    ///
332    /// # Errors
333    ///
334    /// [`GffDocumentError::Syntax`] when the text is not an address, and
335    /// [`GffDocumentError::Path`] when it names nothing, names a field carried
336    /// more than once, or indexes past the end of a list.
337    pub fn remove(&mut self, path: &str) -> Result<GffValue, GffDocumentError> {
338        let path = GffPath::from_str(path)?;
339        Ok(self.remove_at(&path)?)
340    }
341
342    /// Replaces the value at `at`, taking the address and the value as they
343    /// stand.
344    ///
345    /// The typed counterpart to [`set`](Self::set), for a caller that already
346    /// holds both. Reverting a change-review row is the case this exists for:
347    /// [`changes`](Self::changes) is keyed by [`GffPath`] and hands back a
348    /// [`GffValue`], so rendering that address back to text and picking a
349    /// Rust type to put the value through would be undoing work twice.
350    ///
351    /// # Errors
352    ///
353    /// [`GffPathError`] when the field is absent, is carried more than once,
354    /// or stores a different width.
355    pub fn set_at(&mut self, at: &GffPath, value: GffValue) -> Result<GffValue, GffPathError> {
356        self.edit(at.clone(), Subtree::Replaced, |root| root.set(at, value))
357    }
358
359    /// Adds the field `at` names, taking the value as it stands.
360    ///
361    /// The typed counterpart to [`insert`](Self::insert), and the other half
362    /// of reverting a row: a change whose `before` was `None` reverts by
363    /// removing, and one whose `current` is `None` reverts by inserting what
364    /// was there.
365    ///
366    /// # Errors
367    ///
368    /// [`GffPathError`] when the parent does not resolve or the label is
369    /// already there.
370    ///
371    /// A field the opened file carried goes back where the file had it. A
372    /// struct stores its fields in order and the writer emits them in that
373    /// order, so putting one back on the end would rewrite the file from the
374    /// hole onwards. A field the file never had is appended, there being no
375    /// position to restore.
376    pub fn insert_at(&mut self, at: &GffPath, value: GffValue) -> Result<(), GffPathError> {
377        let position = self.position_in_source(at).unwrap_or(usize::MAX);
378        self.edit(at.clone(), Subtree::Replaced, |root| {
379            root.insert_at_position(at, value, position)
380        })
381    }
382
383    /// Where the opened file kept the field `at` names.
384    ///
385    /// Reparses, which costs a parse of the whole document. Only inserts pay
386    /// it, and an insert is a thing somebody clicked rather than something a
387    /// frame does.
388    fn position_in_source(&self, at: &GffPath) -> Option<usize> {
389        Self::open(&self.source).ok()?.gff.root.position(at)
390    }
391
392    /// Removes what `at` names, returning it.
393    ///
394    /// The typed counterpart to [`remove`](Self::remove).
395    ///
396    /// # Errors
397    ///
398    /// [`GffPathError`] when it names nothing, names a field carried more than
399    /// once, or indexes past the end of a list.
400    pub fn remove_at(&mut self, at: &GffPath) -> Result<GffValue, GffPathError> {
401        let node = Self::records_at(at);
402        self.edit(node, Subtree::Replaced, |root| root.remove(at))
403    }
404
405    /// Appends `element` to the list `path` names, returning its index.
406    ///
407    /// The element carries its own struct id and choosing it is the caller's,
408    /// because several lists have the engine skip an element whose id is wrong
409    /// and which id belongs to which list is per-format knowledge this layer
410    /// does not hold.
411    ///
412    /// # Errors
413    ///
414    /// [`GffDocumentError::Syntax`] when the text is not an address, and
415    /// [`GffDocumentError::Path`] when it does not name a list of this tree.
416    pub fn push_element(
417        &mut self,
418        path: &str,
419        element: GffStruct,
420    ) -> Result<usize, GffDocumentError> {
421        let path = GffPath::from_str(path)?;
422        Ok(self.push_element_at(&path, element)?)
423    }
424
425    /// Appends `element` to the list `at` names, returning its index.
426    ///
427    /// The typed counterpart to [`push_element`](Self::push_element). Choosing
428    /// the element's struct id stays the caller's, for the reason given there.
429    ///
430    /// # Errors
431    ///
432    /// [`GffPathError`] when `at` does not name a list of this tree.
433    pub fn push_element_at(
434        &mut self,
435        at: &GffPath,
436        element: GffStruct,
437    ) -> Result<usize, GffPathError> {
438        self.edit(at.clone(), Subtree::Kept, |root| {
439            root.push_element(at, element)
440        })
441    }
442
443    /// Sets the struct id of the list element `at` names, returning the old one.
444    ///
445    /// Some formats carry meaning in an element's id rather than in any field.
446    /// A UTC's equipment slot **is** its `Equip_ItemList` element's struct id,
447    /// so moving an item between slots is this call rather than a removal and
448    /// an append. Which ids a list accepts stays the caller's to know, for the
449    /// reason it is on [`push_element`](Self::push_element).
450    ///
451    /// # The change lands on the list, not on the element
452    ///
453    /// Not a choice about where it reads best. A [`Change`] holds a
454    /// [`GffValue`], a list holds its elements as [`GffStruct`], and no value
455    /// in the tree stands for one element. That is the same shortfall
456    /// [`get`](Self::get) refuses an index-terminated address over. The list is
457    /// the shallowest node that has a value, and an element's id is inside it,
458    /// so the comparison already sees an id change with nothing new to observe
459    /// it with.
460    ///
461    /// Nothing inside the element moves, so a change recorded under it stays
462    /// recorded.
463    ///
464    /// # Errors
465    ///
466    /// [`GffPathError`] when `at` does not end in a list index, or indexes past
467    /// the end of the list.
468    pub fn set_struct_id_at(&mut self, at: &GffPath, struct_id: i32) -> Result<i32, GffPathError> {
469        self.edit(Self::records_at(at), Subtree::Kept, |root| {
470            root.set_struct_id(at, struct_id)
471        })
472    }
473
474    /// The address a mutation on `path` is recorded against.
475    ///
476    /// The path itself, except for a list element: removing `ItemList[1]`
477    /// shifts every sibling after it, so `ItemList[1]` now holds what
478    /// `ItemList[2]` did and recording against it would describe a change
479    /// nobody made. The list is the shallowest node whose content actually
480    /// differs, so that is where it lands.
481    ///
482    /// Public because a caller building an undo stack has to read the same
483    /// value either side of a mutation, and it can only do that if it knows
484    /// where the mutation will land. Working it out a second time from the
485    /// same rule is how the two come to disagree about a list element.
486    pub fn records_at(path: &GffPath) -> GffPath {
487        match path.segments().last() {
488            Some(GffPathSegment::Index(_)) => path
489                .parent()
490                .expect("a path with a last segment has a parent"),
491            _ => path.clone(),
492        }
493    }
494
495    /// Applies one mutation and records what it did.
496    ///
497    /// The only place the tree is mutated. `apply` gets the root, and the
498    /// value at `at` is read either side of it, so a mutator added later
499    /// cannot record nothing: it has no other way to reach a `&mut GffStruct`.
500    ///
501    /// A recording that comes back to what the file held is dropped rather
502    /// than kept as a no-change row, which is what collapses a set-and-set-back
503    /// and an append-then-remove to nothing at all.
504    ///
505    /// # What a mutation does to what is already recorded under it
506    ///
507    /// `scope` is the mutator's own statement about its subtree, because the
508    /// address alone does not say. Removing a list shifts and discards
509    /// everything under it; appending to the same list leaves every existing
510    /// element where it was. Both are mutations at the list's address and only
511    /// one of them makes a change recorded under it obsolete.
512    fn edit<T>(
513        &mut self,
514        at: GffPath,
515        scope: Subtree,
516        apply: impl FnOnce(&mut super::GffStruct) -> Result<T, GffPathError>,
517    ) -> Result<T, GffPathError> {
518        // Descendants sort contiguously after their ancestor with nothing able
519        // to sort between, so the range stops at the first path that is not
520        // under `at`.
521        let obsolete: Vec<GffPath> = match scope {
522            Subtree::Kept => Vec::new(),
523            Subtree::Replaced => self
524                .changes
525                .range(at.clone()..)
526                .take_while(|(path, _)| path.starts_with(&at))
527                .filter(|(path, _)| **path != at)
528                .map(|(path, _)| path.clone())
529                .collect(),
530        };
531
532        let before = if obsolete.is_empty() {
533            // Nothing under `at` has been edited, so the tree still holds what
534            // the file did.
535            self.gff.root.get(&at).ok().cloned()
536        } else {
537            // Something under `at` has been edited, so the tree does not. Read
538            // the opened file instead: `before` means what the file held, and a
539            // subtree carrying an interim edit is not that.
540            //
541            // Reparsing costs a parse of the whole document, and this is the
542            // only path that pays it: replacing a subtree somebody had already
543            // edited inside.
544            Self::open(&self.source)
545                .ok()
546                .and_then(|opened| opened.gff.root.get(&at).ok().cloned())
547        };
548
549        let outcome = apply(&mut self.gff.root)?;
550        // After `apply` succeeded, so it rises exactly when the tree moved.
551        self.revision += 1;
552        let current = self.gff.root.get(&at).ok().cloned();
553
554        // Dropped after the mutation rather than before, so a failing `apply`
555        // leaves the map as it was.
556        for path in obsolete {
557            self.changes.remove(&path);
558        }
559
560        match self.changes.entry(at) {
561            std::collections::btree_map::Entry::Occupied(mut entry) => {
562                // `before` stays what the file held; only the near side moves.
563                entry.get_mut().current = current;
564                if entry.get().before == entry.get().current {
565                    entry.remove();
566                }
567            }
568            std::collections::btree_map::Entry::Vacant(entry) => {
569                if before != current {
570                    entry.insert(Change { before, current });
571                }
572            }
573        }
574        Ok(outcome)
575    }
576
577    /// The file to write back.
578    ///
579    /// The bytes this document was opened with, byte for byte, until an edit
580    /// has landed. After one, the tree re-serialised.
581    ///
582    /// # Errors
583    ///
584    /// Returns [`GffBinaryError`] when the edited tree cannot be encoded, which
585    /// a tree read out of a file and edited through these mutators cannot
586    /// reach: everything they store is a value this crate can encode.
587    pub fn save(&self) -> Result<Vec<u8>, GffBinaryError> {
588        if self.is_edited() {
589            write_gff_to_vec(&self.gff)
590        } else {
591            Ok(self.source.clone())
592        }
593    }
594}
595
596#[cfg(test)]
597mod tests {
598    use super::*;
599    use crate::gff::GffField;
600    use crate::gff_label;
601
602    const UTW: &[u8] = include_bytes!(concat!(
603        env!("CARGO_MANIFEST_DIR"),
604        "/../../fixtures/test.utw"
605    ));
606
607    fn open() -> GffDocument {
608        GffDocument::open(UTW).expect("the fixture is a GFF")
609    }
610
611    /// Every label the file arrived with, so a test can say what an edit left
612    /// alone rather than only what it changed.
613    fn labels(document: &GffDocument) -> Vec<String> {
614        document
615            .gff()
616            .root
617            .fields
618            .iter()
619            .map(|field| field.label.to_string())
620            .collect()
621    }
622
623    /// A field the file carried goes back where the file had it.
624    ///
625    /// A struct stores its fields in order and the writer emits them in that
626    /// order, so restoring one by appending rewrites the file from the hole
627    /// onwards. That is what undoing a deletion in the editor produced, and
628    /// what reverting a removed row produced, both of them through here.
629    #[test]
630    fn a_removed_field_goes_back_where_it_was() {
631        let mut document = open();
632        let order = labels(&document);
633        let middle = order.len() / 2;
634        let at = GffPath::from_str(&order[middle]).expect("a label is an address");
635
636        let held = document.remove_at(&at).expect("the field is there");
637        assert_ne!(labels(&document), order, "nothing was removed");
638
639        document.insert_at(&at, held).expect("it goes back");
640        assert_eq!(
641            labels(&document),
642            order,
643            "a restored field landed somewhere else"
644        );
645    }
646
647    /// And the bytes agree, which is the claim that actually matters: what the
648    /// editor writes after an undo has to be what it would have written without
649    /// the round trip.
650    ///
651    /// Measured against a second document carrying only the unrelated edit,
652    /// rather than against the file. A removal and its undo cancel out in the
653    /// change map, so on their own they leave the document unedited and `save`
654    /// hands back the source without encoding anything: comparing that to the
655    /// file passes whatever the field order is. The unrelated edit is what
656    /// forces the writer to run.
657    #[test]
658    fn a_field_removed_and_put_back_encodes_where_it_started() {
659        let order = labels(&open());
660        let moved = GffPath::from_str(&order[1]).expect("a label is an address");
661        let elsewhere = GffPath::from_str(&order[order.len() - 1]).expect("an address");
662
663        let dirty = |document: &mut GffDocument| {
664            let held = document.get_at(&elsewhere).expect("it is there").clone();
665            document
666                .remove_at(&elsewhere)
667                .expect("something to make this document encode");
668            held
669        };
670
671        let mut untouched = open();
672        dirty(&mut untouched);
673
674        let mut round_tripped = open();
675        dirty(&mut round_tripped);
676        let held = round_tripped.remove_at(&moved).expect("the field is there");
677        round_tripped.insert_at(&moved, held).expect("it goes back");
678
679        assert!(round_tripped.is_edited(), "nothing would be encoded");
680        assert_eq!(
681            round_tripped.save().expect("it encodes"),
682            untouched.save().expect("it encodes"),
683            "a removal and its undo did not cancel out in the bytes"
684        );
685    }
686
687    /// The revision rises on a mutation that records no change.
688    ///
689    /// The property that makes it usable as a cache key and the reason it is
690    /// not derived from the change map. Setting a value back to what the file
691    /// held prunes the row, so the map looks exactly as it did while the tree
692    /// has moved twice; a reader keyed on the map would hand back rows built
693    /// from a tree that no longer exists.
694    #[test]
695    fn a_mutation_that_records_nothing_still_counts() {
696        let mut document = open();
697        let at = GffPath::from_str(&labels(&document)[1]).expect("a label is an address");
698        let held = document.get_at(&at).expect("a field").clone();
699
700        // Removed and put back rather than set twice, so the mutation does not
701        // depend on guessing the field's width.
702        let start = document.revision();
703        document.remove_at(&at).expect("the field is there");
704        document.insert_at(&at, held).expect("it goes back");
705
706        assert!(
707            document.changes().is_empty(),
708            "the round trip left a change, so this is not testing the pruned case"
709        );
710        assert_eq!(
711            document.revision(),
712            start + 2,
713            "a pruned mutation did not count"
714        );
715    }
716
717    /// A failed mutation does not count, since the tree did not move.
718    #[test]
719    fn a_refused_mutation_does_not_count() {
720        let mut document = open();
721        let start = document.revision();
722        document
723            .set_at(
724                &GffPath::from_str("NoSuchField").expect("an address"),
725                GffValue::UInt32(1),
726            )
727            .expect_err("the address resolves to nothing");
728        assert_eq!(document.revision(), start, "a refused edit counted");
729    }
730
731    /// A field the file never had has no position to restore, so it goes on
732    /// the end. This is the add-a-field case rather than the undo case.
733    #[test]
734    fn a_field_the_file_never_had_is_appended() {
735        let mut document = open();
736        let at = GffPath::from_str("RakataMarker").expect("an address");
737
738        document
739            .insert_at(&at, GffValue::UInt32(7))
740            .expect("it is new");
741
742        assert_eq!(
743            labels(&document).last().map(String::as_str),
744            Some("RakataMarker"),
745            "a new field did not go on the end"
746        );
747    }
748
749    #[test]
750    fn a_document_opens_unedited_and_hands_back_the_bytes_it_was_given() {
751        let document = open();
752        assert!(!document.is_edited());
753        assert_eq!(document.save().expect("no edit, no encode"), UTW);
754    }
755
756    #[test]
757    fn inserting_adds_a_field_and_refuses_one_that_is_there() {
758        let mut document = open();
759        let before = labels(&document);
760        assert!(!before.contains(&"RakataMarker".to_owned()));
761
762        document
763            .insert("RakataMarker", 7_u8)
764            .expect("the label is not there yet");
765        assert!(document.is_edited());
766        assert_eq!(
767            document.get("RakataMarker").expect("just inserted"),
768            &GffValue::UInt8(7)
769        );
770
771        // Every label the file arrived with is still there, in order, with the
772        // new one after them.
773        let after = labels(&document);
774        assert_eq!(&after[..before.len()], &before[..]);
775        assert_eq!(after.last().map(String::as_str), Some("RakataMarker"));
776
777        assert!(
778            document.insert("RakataMarker", 8_u8).is_err(),
779            "a second insert would append a duplicate the first would win over"
780        );
781    }
782
783    #[test]
784    fn removing_takes_a_field_away_and_hands_it_back() {
785        let mut document = open();
786        let before = labels(&document);
787        let target = before.first().expect("the fixture holds fields").clone();
788
789        let removed = document.remove(&target).expect("the field is there");
790        assert!(document.is_edited());
791        assert!(
792            !matches!(removed, GffValue::List(_)),
793            "the fixture's first field is a scalar"
794        );
795        assert!(!labels(&document).contains(&target));
796        assert!(document.get(&target).is_err(), "it is gone");
797    }
798
799    /// A document holding a list, authored rather than read, because the
800    /// committed scalar fixtures carry none.
801    fn with_a_list() -> GffDocument {
802        let gff = Gff::new(
803            *b"UTW ",
804            GffStruct {
805                struct_id: -1,
806                fields: vec![
807                    GffField {
808                        label: gff_label!("Mark"),
809                        value: GffValue::UInt32(5),
810                    },
811                    GffField {
812                        label: gff_label!("RakataList"),
813                        value: GffValue::List(Vec::new()),
814                    },
815                    GffField {
816                        label: gff_label!("Nested"),
817                        value: GffValue::Struct(Box::new(GffStruct {
818                            struct_id: 0,
819                            fields: vec![GffField {
820                                label: gff_label!("Inner"),
821                                value: GffValue::UInt32(7),
822                            }],
823                        })),
824                    },
825                ],
826            },
827        );
828        let bytes = write_gff_to_vec(&gff).expect("a list of nothing encodes");
829        GffDocument::open(&bytes).expect("what this crate wrote, it reads")
830    }
831
832    fn element(mark: u8) -> GffStruct {
833        GffStruct {
834            struct_id: 3,
835            fields: vec![GffField {
836                label: gff_label!("Mark"),
837                value: GffValue::UInt8(mark),
838            }],
839        }
840    }
841
842    #[test]
843    fn pushing_an_element_appends_it_and_says_where_it_landed() {
844        let mut document = with_a_list();
845        assert!(!document.is_edited());
846
847        assert_eq!(document.push_element("RakataList", element(1)), Ok(0));
848        assert_eq!(document.push_element("RakataList", element(2)), Ok(1));
849        assert!(document.is_edited());
850
851        assert_eq!(document.get("RakataList[1].Mark"), Ok(&GffValue::UInt8(2)));
852        assert!(
853            document.push_element("Nope", element(3)).is_err(),
854            "a path naming no list is refused rather than creating one"
855        );
856    }
857
858    /// Removing by index takes the element out and leaves the ones around it,
859    /// which is what an inventory row deletion is.
860    #[test]
861    fn removing_an_element_leaves_its_siblings_where_they_were() {
862        let mut document = with_a_list();
863        for mark in 1..=3 {
864            document
865                .push_element("RakataList", element(mark))
866                .expect("the list is there");
867        }
868
869        let removed = document
870            .remove("RakataList[1]")
871            .expect("the element is there");
872        assert!(matches!(removed, GffValue::Struct(_)));
873        assert_eq!(document.get("RakataList[0].Mark"), Ok(&GffValue::UInt8(1)));
874        assert_eq!(document.get("RakataList[1].Mark"), Ok(&GffValue::UInt8(3)));
875        assert!(document.get("RakataList[2].Mark").is_err());
876    }
877
878    /// The point of the document. An edit through any mutator leaves every
879    /// other field exactly where it was, including ones no view models.
880    #[test]
881    fn an_edit_through_any_mutator_leaves_the_rest_of_the_file_alone() {
882        let untouched = open();
883        for edit in ["insert", "remove"] {
884            let mut document = open();
885            let before = labels(&document);
886            match edit {
887                "insert" => document.insert("RakataMarker", 1_u8).expect("insert"),
888                "remove" => {
889                    document.remove(&before[0]).expect("remove");
890                }
891                other => panic!("{other}"),
892            }
893            for label in &before {
894                let changed = edit == "remove" && *label == before[0];
895                assert_eq!(
896                    document.get(label).is_ok(),
897                    !changed,
898                    "`{label}` after an {edit}"
899                );
900                if !changed {
901                    assert_eq!(
902                        document.get(label).expect("still there"),
903                        untouched.get(label).expect("was there"),
904                        "`{label}` changed value during an {edit}"
905                    );
906                }
907            }
908        }
909    }
910
911    #[test]
912    fn every_mutator_makes_save_re_serialise() {
913        let mut inserted = open();
914        inserted.insert("RakataMarker", 1_u8).expect("insert");
915        assert!(inserted.is_edited());
916        assert_ne!(inserted.save().expect("encode"), UTW);
917
918        let mut removed = open();
919        let first = labels(&removed)[0].clone();
920        removed.remove(&first).expect("remove");
921        assert!(removed.is_edited());
922        assert_ne!(removed.save().expect("encode"), UTW);
923    }
924
925    // ------------------------------------------------------------------
926    // What the document records, which is #101.
927    // ------------------------------------------------------------------
928
929    /// The bug this replaced. `set` flagged the file edited whenever it ran,
930    /// so a UI writing back the value already in a text field made `save`
931    /// re-serialise a file nothing had changed — and re-serialising is not
932    /// byte-faithful for most of the corpus.
933    #[test]
934    fn writing_back_the_value_a_field_already_held_is_not_an_edit() {
935        let mut document = with_a_list();
936        let untouched = document.save().expect("nothing changed yet");
937
938        document.set("Mark", 5_u32).expect("Mark is a UInt32");
939
940        assert!(
941            !document.is_edited(),
942            "a value written back unchanged left the file counting as edited"
943        );
944        assert!(document.changes().is_empty());
945        assert_eq!(document.save().expect("still nothing changed"), untouched);
946    }
947
948    #[test]
949    fn setting_twice_records_one_row_against_what_the_file_held() {
950        let mut document = with_a_list();
951        document.set("Mark", 111_u32).expect("set");
952        document.set("Mark", 222_u32).expect("set again");
953
954        assert_eq!(document.changes().len(), 1);
955        let change = document.changes().values().next().expect("one row");
956        assert_eq!(change.before, Some(GffValue::UInt32(5)));
957        assert_eq!(change.current, Some(GffValue::UInt32(222)));
958    }
959
960    #[test]
961    fn setting_a_field_back_to_what_it_held_drops_the_row() {
962        let mut document = with_a_list();
963        document.set("Mark", 99_u32).expect("set");
964        assert_eq!(document.changes().len(), 1);
965
966        document.set("Mark", 5_u32).expect("set back");
967        assert!(document.changes().is_empty(), "the row survived the undo");
968        assert!(!document.is_edited());
969    }
970
971    #[test]
972    fn appending_an_element_and_removing_it_again_nets_to_nothing() {
973        let mut document = with_a_list();
974        let at = document
975            .push_element("RakataList", element(1))
976            .expect("the list is there");
977        assert_eq!(document.changes().len(), 1);
978
979        document
980            .remove(&format!("RakataList[{at}]"))
981            .expect("the element is there");
982        assert!(document.changes().is_empty(), "the list did not come back");
983        assert!(!document.is_edited());
984    }
985
986    /// A list mutation records against the list rather than the index the
987    /// caller named: removing an element shifts its siblings, so the index no
988    /// longer holds what it did and a row against it would describe a change
989    /// nobody made.
990    #[test]
991    fn a_list_mutation_is_recorded_against_the_list() {
992        let mut document = with_a_list();
993        for mark in 1..=3 {
994            document
995                .push_element("RakataList", element(mark))
996                .expect("the list is there");
997        }
998        document
999            .remove("RakataList[0]")
1000            .expect("the element is there");
1001
1002        let keys: Vec<String> = document.changes().keys().map(ToString::to_string).collect();
1003        assert_eq!(keys, vec!["RakataList".to_owned()]);
1004    }
1005
1006    /// A document whose file already carries one element, so an id change is
1007    /// measured against what was read rather than against an append.
1008    fn with_an_element() -> GffDocument {
1009        let mut document = with_a_list();
1010        document
1011            .push_element("RakataList", element(1))
1012            .expect("the list is there");
1013        let bytes = document.save().expect("an element encodes");
1014        GffDocument::open(&bytes).expect("what this crate wrote, it reads")
1015    }
1016
1017    /// An element's id is data in several formats, so changing it has to record
1018    /// like any other edit and prune like one.
1019    #[test]
1020    fn changing_an_element_id_records_against_the_list_and_prunes() {
1021        let mut document = with_an_element();
1022        let at = GffPath::from_str("RakataList[0]").expect("the test's own address");
1023        assert!(!document.is_edited());
1024        assert_eq!(document.gff().root.struct_id(&at), Ok(3));
1025
1026        assert_eq!(document.set_struct_id_at(&at, 9), Ok(3));
1027        assert_eq!(document.gff().root.struct_id(&at), Ok(9));
1028        assert!(
1029            document.is_edited(),
1030            "an id change left nothing for the review pane"
1031        );
1032
1033        // The list, because a change holds a value and no value in the tree
1034        // stands for one element.
1035        let keys: Vec<String> = document.changes().keys().map(ToString::to_string).collect();
1036        assert_eq!(keys, vec!["RakataList".to_owned()]);
1037
1038        document
1039            .set_struct_id_at(&at, 3)
1040            .expect("the element is still there");
1041        assert!(
1042            !document.is_edited(),
1043            "an id set back to what the file held stayed on the review pane"
1044        );
1045    }
1046
1047    /// Changing an id moves nothing inside the element, so an edit already
1048    /// recorded in there is still recorded afterwards.
1049    ///
1050    /// This is the half `Subtree::Kept` decides. Under `Replaced` the inner row
1051    /// would be dropped as obsolete, and reverting the list would then be the
1052    /// only way back to a field the user never meant to touch.
1053    #[test]
1054    fn an_id_change_leaves_an_edit_inside_the_element_alone() {
1055        let mut document = with_an_element();
1056        let at = GffPath::from_str("RakataList[0]").expect("the test's own address");
1057
1058        document
1059            .set("RakataList[0].Mark", 42_u8)
1060            .expect("the element carries a mark");
1061        document
1062            .set_struct_id_at(&at, 9)
1063            .expect("the element is there");
1064
1065        let keys: Vec<String> = document.changes().keys().map(ToString::to_string).collect();
1066        assert_eq!(
1067            keys,
1068            vec!["RakataList".to_owned(), "RakataList[0].Mark".to_owned()],
1069            "the edit inside the element did not survive the id change"
1070        );
1071        assert_eq!(document.get("RakataList[0].Mark"), Ok(&GffValue::UInt8(42)));
1072    }
1073
1074    /// An id belongs to a list element, so an address naming anything else is
1075    /// refused rather than resolved to something nearby.
1076    #[test]
1077    fn only_a_list_element_has_an_id_to_set() {
1078        let mut document = with_an_element();
1079        for text in ["RakataList", "Mark", "Nested.Inner"] {
1080            let at = GffPath::from_str(text).expect("the test's own address");
1081            assert!(
1082                document.set_struct_id_at(&at, 9).is_err(),
1083                "`{text}` is not a list element and took an id anyway"
1084            );
1085        }
1086        assert!(!document.is_edited(), "a refused id change recorded one");
1087    }
1088
1089    /// The addressed forms reach what the text forms reach.
1090    ///
1091    /// Be clear about which half of this is doing work. The equality between
1092    /// `get` and `get_at` cannot fail today, because `get` parses and calls
1093    /// `get_at`, so that line guards the delegation rather than the lookup: it
1094    /// fails only if somebody gives the text form an implementation of its own,
1095    /// which is the drift the pairing exists to stop. The assertions pinning
1096    /// actual values and ids are the ones testing behaviour.
1097    #[test]
1098    fn the_text_forms_and_the_addressed_forms_agree() {
1099        let mut document = with_an_element();
1100        let at = GffPath::from_str("RakataList[0].Mark").expect("the test's own address");
1101
1102        assert_eq!(
1103            document.get_at(&at).ok(),
1104            document.get(&at.to_string()).ok()
1105        );
1106        assert_eq!(document.get_at(&at), Ok(&GffValue::UInt8(1)));
1107
1108        let list = GffPath::from_str("RakataList").expect("the test's own address");
1109        assert_eq!(document.push_element_at(&list, element(2)), Ok(1));
1110        assert_eq!(document.push_element(&list.to_string(), element(3)), Ok(2));
1111
1112        // And the id reader reaches what the mutator writes, without going
1113        // through the tree behind the document's back.
1114        let second = GffPath::from_str("RakataList[1]").expect("the test's own address");
1115        assert_eq!(document.struct_id_at(&second), Ok(3));
1116        document
1117            .set_struct_id_at(&second, 11)
1118            .expect("the element is there");
1119        assert_eq!(document.struct_id_at(&second), Ok(11));
1120    }
1121
1122    /// An address that already parsed cannot fail to parse, so the `_at` forms
1123    /// report the path error directly.
1124    ///
1125    /// The reason the two families carry different error types rather than one
1126    /// with an arm that never fires on half its callers.
1127    #[test]
1128    fn an_addressed_form_reports_a_path_error_with_no_syntax_arm() {
1129        let document = with_an_element();
1130        let missing = GffPath::from_str("Nope").expect("the test's own address");
1131
1132        let refused: Result<&GffValue, GffPathError> = document.get_at(&missing);
1133        assert!(refused.is_err());
1134
1135        // The text form wraps the same failure, plus a syntax arm this one has
1136        // no way to reach.
1137        assert!(matches!(
1138            document.get("Nope"),
1139            Err(GffDocumentError::Path(_))
1140        ));
1141        assert!(matches!(
1142            document.get("Nope["),
1143            Err(GffDocumentError::Syntax(_))
1144        ));
1145    }
1146
1147    /// A list that is not there can be created and then filled.
1148    ///
1149    /// `insert` takes a scalar, so a creature with no `ItemList` is given one
1150    /// through `insert_at`, which takes a [`GffValue`]. The two calls have to
1151    /// work in this order and nothing else asserts that.
1152    #[test]
1153    fn a_list_that_is_not_there_can_be_created_and_then_appended_to() {
1154        let mut document = open();
1155        let at = GffPath::from_str("ItemList").expect("the test's own address");
1156        assert!(
1157            document.get_at(&at).is_err(),
1158            "the fixture is supposed to carry no such list"
1159        );
1160
1161        // Appending is refused while there is nothing to append to, and the
1162        // refusal leaves no change: the funnel returns before recording, and
1163        // nothing moved at the address either way.
1164        let refused = document
1165            .push_element_at(&at, element(1))
1166            .expect_err("there is no list to append to yet");
1167        assert!(
1168            matches!(refused, GffPathError::NoSuchField { .. }),
1169            "{refused}"
1170        );
1171        assert!(
1172            !document.is_edited(),
1173            "a refused append left the document looking edited"
1174        );
1175
1176        document
1177            .insert_at(&at, GffValue::List(Vec::new()))
1178            .expect("the root takes a new label");
1179        assert_eq!(document.push_element_at(&at, element(1)), Ok(0));
1180        assert_eq!(document.get_at(&at), Ok(&GffValue::List(vec![element(1)])));
1181    }
1182
1183    /// One mutation to run against a document.
1184    type Step = Box<dyn Fn(&mut GffDocument)>;
1185
1186    /// Every way the API can mutate a document, as sequences to run against
1187    /// one. Named so a failure says which program disagreed.
1188    fn programs() -> Vec<(&'static str, Vec<Step>)> {
1189        fn set(path: &'static str, to: u32) -> Step {
1190            Box::new(move |d: &mut GffDocument| {
1191                d.set(path, to).expect("the field is there");
1192            })
1193        }
1194        fn push(mark: u8) -> Step {
1195            Box::new(move |d: &mut GffDocument| {
1196                d.push_element("RakataList", element(mark))
1197                    .expect("the list is there");
1198            })
1199        }
1200        fn drop_at(path: &'static str) -> Step {
1201            Box::new(move |d: &mut GffDocument| {
1202                d.remove(path).expect("it is there");
1203            })
1204        }
1205        fn add(path: &'static str) -> Step {
1206            Box::new(move |d: &mut GffDocument| {
1207                d.insert(path, 1_u32).expect("it is not there yet");
1208            })
1209        }
1210
1211        vec![
1212            ("nothing", vec![]),
1213            ("one scalar", vec![set("Mark", 9)]),
1214            ("scalar twice", vec![set("Mark", 9), set("Mark", 10)]),
1215            ("scalar and back", vec![set("Mark", 9), set("Mark", 5)]),
1216            ("nested scalar", vec![set("Nested.Inner", 8)]),
1217            (
1218                "nested and back",
1219                vec![set("Nested.Inner", 8), set("Nested.Inner", 7)],
1220            ),
1221            ("insert", vec![add("Fresh")]),
1222            ("insert then remove", vec![add("Fresh"), drop_at("Fresh")]),
1223            ("remove a field", vec![drop_at("Mark")]),
1224            ("push one", vec![push(1)]),
1225            ("push and pop", vec![push(1), drop_at("RakataList[0]")]),
1226            (
1227                "push three, drop the first",
1228                vec![push(1), push(2), push(3), drop_at("RakataList[0]")],
1229            ),
1230            (
1231                "everything at once",
1232                vec![
1233                    set("Mark", 9),
1234                    set("Nested.Inner", 8),
1235                    add("Fresh"),
1236                    push(1),
1237                ],
1238            ),
1239            (
1240                "everything, then undone",
1241                vec![
1242                    set("Mark", 9),
1243                    set("Nested.Inner", 8),
1244                    add("Fresh"),
1245                    push(1),
1246                    set("Mark", 5),
1247                    set("Nested.Inner", 7),
1248                    drop_at("Fresh"),
1249                    drop_at("RakataList[0]"),
1250                ],
1251            ),
1252        ]
1253    }
1254
1255    /// The addresses at which two trees differ, computed the slow way.
1256    ///
1257    /// Reports the shallowest node whose content differs: a changed scalar
1258    /// inside a nested struct is reported at the scalar, and a list that
1259    /// gained or lost an element is reported at the list rather than at each
1260    /// shifted index.
1261    fn diff_trees(before: &Gff, after: &Gff) -> Vec<String> {
1262        fn walk(before: &GffStruct, after: &GffStruct, at: &GffPath, out: &mut Vec<String>) {
1263            let mut labels: Vec<_> = before
1264                .fields
1265                .iter()
1266                .chain(after.fields.iter())
1267                .map(|field| field.label)
1268                .collect();
1269            labels.sort();
1270            labels.dedup();
1271
1272            for label in labels {
1273                let here = at.then(GffPathSegment::Field(label));
1274                let find = |s: &GffStruct| {
1275                    s.fields
1276                        .iter()
1277                        .find(|field| field.label == label)
1278                        .map(|field| field.value.clone())
1279                };
1280                match (find(before), find(after)) {
1281                    (Some(was), Some(now)) if was == now => {}
1282                    (Some(GffValue::Struct(was)), Some(GffValue::Struct(now))) => {
1283                        walk(&was, &now, &here, out);
1284                    }
1285                    (None, None) => {}
1286                    _ => out.push(here.to_string()),
1287                }
1288            }
1289        }
1290
1291        let mut out = Vec::new();
1292        walk(&before.root, &after.root, &GffPath::default(), &mut out);
1293        out.sort();
1294        out
1295    }
1296
1297    /// The guard. Whatever the recording says, a diff computed by reparsing
1298    /// the bytes this was opened from and walking both trees has to say the
1299    /// same thing.
1300    ///
1301    /// That derived diff is exactly what recording exists to avoid running on
1302    /// every redraw. Keeping it as a check rather than as the implementation
1303    /// is what catches a mutator that forgets to record.
1304    #[test]
1305    fn the_recorded_changes_match_a_diff_of_the_two_trees() {
1306        for (name, steps) in programs() {
1307            let mut document = with_a_list();
1308            for step in &steps {
1309                step(&mut document);
1310            }
1311            let opened = read_gff_from_bytes(&document.source).expect("the source parses");
1312            let derived = diff_trees(&opened, &document.gff);
1313            let recorded: Vec<String> =
1314                document.changes().keys().map(ToString::to_string).collect();
1315            assert_eq!(recorded, derived, "`{name}`: recorded and derived disagree");
1316            assert_eq!(
1317                document.is_edited(),
1318                !derived.is_empty(),
1319                "`{name}`: is_edited disagrees with the tree"
1320            );
1321        }
1322    }
1323
1324    /// The revert control the change-review pane needs, done with the values
1325    /// and addresses the pane already holds.
1326    ///
1327    /// Reverting every row has to leave the document exactly as opened, which
1328    /// is the property that makes the pane trustworthy: the map empties rather
1329    /// than filling with rows that cancel out.
1330    #[test]
1331    fn every_row_reverts_through_the_address_it_is_keyed_by() {
1332        let mut document = open();
1333        document
1334            .set("Tag", "edited".to_owned())
1335            .expect("the fixture carries a Tag");
1336        document
1337            .insert("RakataMarker", 7_u32)
1338            .expect("a label the file does not carry");
1339        let removed_from = "LocalizedName";
1340        document
1341            .remove(removed_from)
1342            .expect("the fixture carries a localized name");
1343
1344        let rows: Vec<(GffPath, Change)> = document
1345            .changes()
1346            .iter()
1347            .map(|(path, change)| (path.clone(), change.clone()))
1348            .collect();
1349        assert_eq!(rows.len(), 3, "one row per address touched");
1350
1351        for (path, change) in rows {
1352            match (change.before, change.current) {
1353                (Some(before), Some(_)) => {
1354                    document.set_at(&path, before).expect("changed reverts");
1355                }
1356                (None, Some(_)) => {
1357                    document.remove_at(&path).expect("added reverts");
1358                }
1359                (Some(before), None) => {
1360                    document.insert_at(&path, before).expect("removed reverts");
1361                }
1362                (None, None) => panic!("`{path}` is a row that cannot occur"),
1363            }
1364        }
1365
1366        assert!(
1367            !document.is_edited(),
1368            "reverting every row left {:?}",
1369            document.changes()
1370        );
1371        assert_eq!(
1372            document.save().expect("nothing to encode"),
1373            UTW,
1374            "the bytes came back but not the original ones"
1375        );
1376    }
1377
1378    /// A typed read distinguishes the three ways it can fail.
1379    ///
1380    /// Collapsing "no such field" into "wrong type" is the tempting
1381    /// simplification and it is the one that makes a caller unable to tell an
1382    /// ordinary absence from a file that is not what it claimed to be.
1383    #[test]
1384    fn a_typed_read_keeps_absent_and_wrong_apart() {
1385        let document = open();
1386
1387        // The fixture's tag is a string, and reads as one.
1388        let tag: String = document.get_as("Tag").expect("the fixture carries a tag");
1389        assert!(!tag.is_empty());
1390
1391        // A label the file does not carry is a path failure.
1392        assert!(matches!(
1393            document.get_as::<String>("RakataMissing"),
1394            Err(GffDocumentError::Path(_))
1395        ));
1396
1397        // A label it does carry, read as something it is not.
1398        assert!(matches!(
1399            document.get_as::<[f32; 4]>("Tag"),
1400            Err(GffDocumentError::WrongType { .. })
1401        ));
1402
1403        // And text that is not an address at all is a third thing again.
1404        assert!(matches!(
1405            document.get_as::<String>("Tag["),
1406            Err(GffDocumentError::Syntax(_))
1407        ));
1408    }
1409
1410    // -- A mutation at an ancestor of something already recorded --
1411
1412    /// A tree with one list holding one struct, so a change can be recorded
1413    /// inside a container that is then replaced.
1414    fn nested() -> GffDocument {
1415        let mut element = GffStruct::new(0);
1416        element.push_field(gff_label!("Tag"), GffValue::String("a".to_owned()));
1417        let mut root = GffStruct::new(-1);
1418        root.push_field(gff_label!("Items"), GffValue::List(vec![element]));
1419        let bytes = write_gff_to_vec(&Gff {
1420            file_type: *b"GIT ",
1421            root,
1422        })
1423        .expect("a tree this small encodes");
1424        GffDocument::open(&bytes).expect("it reads back")
1425    }
1426
1427    /// Removing a container drops what was recorded inside it.
1428    ///
1429    /// The map claims to describe what saving would do. An entry naming a
1430    /// field inside a list that has been removed describes a field the saved
1431    /// file will not contain, and the review pane built on this would show a
1432    /// row for it.
1433    #[test]
1434    fn removing_a_container_drops_what_was_recorded_inside_it() {
1435        let mut document = nested();
1436        let inner = GffPath::from_str("Items[0].Tag").expect("an address");
1437        let list = GffPath::from_str("Items").expect("an address");
1438
1439        document
1440            .set_at(&inner, GffValue::String("b".to_owned()))
1441            .expect("the field takes it");
1442        document.remove_at(&list).expect("the list goes");
1443
1444        let addresses: Vec<String> = document.changes().keys().map(ToString::to_string).collect();
1445        assert_eq!(
1446            addresses,
1447            ["Items"],
1448            "a change was left recorded inside a container that no longer exists"
1449        );
1450    }
1451
1452    /// And the entry it leaves says what the *file* held, not what the tree
1453    /// held a moment before.
1454    ///
1455    /// `before` is documented as the opened file's value. Read off the tree it
1456    /// would carry the interim edit, which is a value the file never had, and
1457    /// anything later trusting `before` would inherit that.
1458    #[test]
1459    fn the_entry_left_behind_describes_the_opened_file() {
1460        let mut document = nested();
1461        let inner = GffPath::from_str("Items[0].Tag").expect("an address");
1462        let list = GffPath::from_str("Items").expect("an address");
1463        let as_opened = document.get("Items").expect("a list").clone();
1464
1465        document
1466            .set_at(&inner, GffValue::String("b".to_owned()))
1467            .expect("the field takes it");
1468        document.remove_at(&list).expect("the list goes");
1469
1470        let entry = document.changes().get(&list).expect("the list is recorded");
1471        assert_eq!(
1472            entry.before.as_ref(),
1473            Some(&as_opened),
1474            "`before` carried an edit the opened file never had"
1475        );
1476        assert_eq!(entry.current, None, "the list was removed");
1477    }
1478
1479    /// Putting the container back leaves the document clean.
1480    ///
1481    /// A deliberate consequence rather than a side effect: reverting restores
1482    /// the file's version, so an edit made inside the container before it was
1483    /// deleted is discarded. The alternative is a `before` that means "what
1484    /// was there a moment ago" for containers and "what the file held"
1485    /// everywhere else, and one meaning across the map is worth more than the
1486    /// interim edit.
1487    #[test]
1488    fn reverting_the_container_restores_the_file_and_nothing_else() {
1489        let mut document = nested();
1490        let inner = GffPath::from_str("Items[0].Tag").expect("an address");
1491        let list = GffPath::from_str("Items").expect("an address");
1492        let source = document.save().expect("the opened bytes");
1493
1494        document
1495            .set_at(&inner, GffValue::String("b".to_owned()))
1496            .expect("the field takes it");
1497        let removed = document.remove_at(&list).expect("the list goes");
1498        let restore = document
1499            .changes()
1500            .get(&list)
1501            .and_then(|change| change.before.clone())
1502            .expect("the list is recorded");
1503        drop(removed);
1504
1505        document.insert_at(&list, restore).expect("it goes back");
1506
1507        assert!(
1508            !document.is_edited(),
1509            "reverting left the document dirty: {:?}",
1510            document.changes().keys().collect::<Vec<_>>()
1511        );
1512        assert_eq!(
1513            document.save().expect("bytes"),
1514            source,
1515            "the file was not put back as it was opened"
1516        );
1517        assert_eq!(
1518            document.get("Items[0].Tag").expect("the field is back"),
1519            &GffValue::String("a".to_owned()),
1520            "the interim edit survived a revert that was meant to discard it"
1521        );
1522    }
1523
1524    /// Appending to a list leaves what is recorded inside it alone.
1525    ///
1526    /// The case that stops this being a rule about addresses. An append and a
1527    /// removal are both mutations of the list, and only the removal moves what
1528    /// is already in it.
1529    #[test]
1530    fn appending_to_a_list_keeps_what_was_recorded_in_it() {
1531        let mut document = nested();
1532        document
1533            .set_at(
1534                &GffPath::from_str("Items[0].Tag").expect("an address"),
1535                GffValue::String("b".to_owned()),
1536            )
1537            .expect("the field takes it");
1538
1539        document
1540            .push_element("Items", GffStruct::new(0))
1541            .expect("the list takes an element");
1542
1543        assert!(
1544            document
1545                .changes()
1546                .contains_key(&GffPath::from_str("Items[0].Tag").expect("path")),
1547            "appending to a list discarded a change recorded inside it"
1548        );
1549    }
1550}