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}