rakata_formats/gff/path.rs
1//! Path-addressed reads and edits of a loaded [`GffStruct`].
2//!
3//! # Why this exists
4//!
5//! A `Gff` tree could be read and it could be written, with nothing in between.
6//! The only public mutation was [`GffStruct::push_field`], which appends, so the
7//! only way to change a value was to parse into a typed view, mutate that, and
8//! rebuild with `to_gff()`. Rebuilding starts from an empty struct and therefore
9//! drops every field the view does not model. That is right for authoring a new
10//! file and it is data loss for editing one somebody else wrote.
11//!
12//! These operations name a field by path and leave the rest of the tree alone.
13//!
14//! # Replace never appends
15//!
16//! Appending a second copy of a label would leave the original winning on
17//! lookup: the file changes, the behaviour does not, and nothing reports it.
18//! [`set`] replaces in place and refuses a label that is not already there;
19//! [`insert`] adds one and refuses a label that is.
20//!
21//! Keeping them apart is deliberate. A single upsert would turn a mistyped
22//! label into a silently-created field rather than an error.
23//!
24//! [`set`]: GffStruct::set
25//! [`insert`]: GffStruct::insert
26//!
27//! # A label carried twice is an error, not a first match
28//!
29//! A struct really can hold one label several times, and vanilla content does:
30//! every `EntryList` and `ReplyList` node in a `.dlg` carries `SoundExists` six
31//! times over, and nothing on record says why. Resolving that by position
32//! answers a question nobody has established the answer to, so every walk here
33//! reports [`AmbiguousLabel`] instead. Editing all of them is a different
34//! request and needs a different call.
35//!
36//! [`AmbiguousLabel`]: GffPathError::AmbiguousLabel
37//!
38//! # What this does not do
39//!
40//! No validation. Whether a label belongs on this format, whether a value is in
41//! range, whether a required field is missing: all of that is `rakata-lint`'s,
42//! and the schemas it needs live above this crate.
43//!
44//! Struct ids on inserted list elements are the caller's to choose.
45//! [`push_element`](GffStruct::push_element) takes a whole [`GffStruct`], which
46//! carries its own id, because several lists have the engine skip an element
47//! whose id is wrong and which value is right is per-format knowledge. See the
48//! GIT and PTH pages in `docs/src/formats/gff/` for the ones that matter.
49//!
50//! # A list element cannot be inserted at an index, on purpose
51//!
52//! Appending is the only way to add a list element here. Inserting into the
53//! middle would shift every later index, and DLG links address their targets by
54//! index, so a mid-list insertion silently breaks every link past the insertion
55//! point. Nothing else wants it either: GIT lists are typed by struct id rather
56//! than ordered, and inventory order is cosmetic.
57//!
58//! So the omission is a decision. Adding it later as an obvious convenience
59//! would be adding a footgun.
60//!
61//! A struct's *fields* are the opposite case, which is why
62//! [`insert_at_position`] exists beside this. Nothing addresses a field by its
63//! position, so putting one back where it was breaks nothing, and the writer
64//! emits fields in the order the struct holds them: restoring a removed field
65//! on the end rewrites the file from the hole onwards for no reason.
66//!
67//! [`insert_at_position`]: GffStruct::insert_at_position
68//!
69//! # Every walker here is private, and that is load-bearing
70//!
71//! The general shape this could have taken is a `struct_at_mut(path)` handing
72//! back a `&mut GffStruct`, and it is the wrong one. A caller holding a mutable
73//! struct can reach [`GffStruct::push_field`] and append over a label that is
74//! already there, which is the silent-inert-edit this whole module exists to
75//! prevent. Every operation is narrow so that the only ways in are ones that
76//! cannot produce a duplicate label.
77
78use std::fmt;
79use std::str::FromStr;
80
81use thiserror::Error;
82
83use super::label::{GffLabel, GffLabelError};
84use super::{GffField, GffStruct, GffValue};
85use crate::schema::gff_value_type;
86
87/// One step of a [`GffPath`].
88#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
89pub enum GffPathSegment {
90 /// Name a field of the struct currently in hand.
91 Field(GffLabel),
92 /// Take an element of the list currently in hand.
93 Index(usize),
94}
95
96/// An address into a GFF tree, such as `ClassList[0].Class`.
97///
98/// Rendering gives the same dot notation `rakata-lint` already prints in
99/// `LintDiagnostic.fields`, so a consumer wanting to navigate to a finding
100/// rather than display it has one address type to use for both.
101#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
102pub struct GffPath {
103 segments: Vec<GffPathSegment>,
104}
105
106impl GffPath {
107 /// Creates a path from its segments.
108 pub fn new(segments: Vec<GffPathSegment>) -> Self {
109 Self { segments }
110 }
111
112 /// The steps this path takes, in order.
113 pub fn segments(&self) -> &[GffPathSegment] {
114 &self.segments
115 }
116
117 /// Whether this path names nothing.
118 pub fn is_empty(&self) -> bool {
119 self.segments.is_empty()
120 }
121
122 /// This path with its last step dropped, or `None` when it has none.
123 ///
124 /// What a list mutation records against: appending to or removing from a
125 /// list changes the list, and the index the caller named is not where the
126 /// change is once the siblings have shifted.
127 pub fn parent(&self) -> Option<Self> {
128 let (_, rest) = self.segments.split_last()?;
129 Some(Self {
130 segments: rest.to_vec(),
131 })
132 }
133
134 /// Extends this path by one step, which is how a walk builds one.
135 pub fn then(&self, segment: GffPathSegment) -> Self {
136 let mut segments = self.segments.clone();
137 segments.push(segment);
138 Self { segments }
139 }
140
141 /// Whether `prefix` names this node or one of its ancestors.
142 ///
143 /// Segment by segment rather than over the rendered text, and that is the
144 /// whole reason this is a method. `"ItemList".starts_with("ItemLis")` is
145 /// true and `ItemList` is not under `ItemLis`, so a prefix test written on
146 /// the strings answers yes for a sibling whose label merely begins the same
147 /// way. Comparing segments cannot make that mistake, because a label is
148 /// matched whole.
149 ///
150 /// Reflexive, matching the slice method it is built on: a path starts with
151 /// itself. Callers wanting proper descendants exclude equality themselves,
152 /// which is what the tree does when deciding whether a collapsed node hides
153 /// a row, since a collapsed node has to keep drawing itself.
154 ///
155 /// Ordering makes this cheap to use over a set. [`GffPath`] sorts by
156 /// segments, so every descendant of a path sorts contiguously after it with
157 /// nothing able to sort between; a `BTreeMap` of changes answers "anything
158 /// under here" with one `range` and this test on the first entry.
159 pub fn starts_with(&self, prefix: &Self) -> bool {
160 self.segments.starts_with(&prefix.segments)
161 }
162
163 /// Renders the path up to and including `upto` segments.
164 ///
165 /// Used so an error names where the walk stopped rather than the whole
166 /// path, which is what tells a caller which step was wrong.
167 fn prefix(&self, upto: usize) -> String {
168 GffPath::new(self.segments[..upto.min(self.segments.len())].to_vec()).to_string()
169 }
170}
171
172impl fmt::Display for GffPath {
173 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174 let mut first = true;
175 for segment in &self.segments {
176 match segment {
177 GffPathSegment::Field(label) => {
178 if !first {
179 write!(f, ".")?;
180 }
181 write!(f, "{label}")?;
182 }
183 GffPathSegment::Index(index) => write!(f, "[{index}]")?,
184 }
185 first = false;
186 }
187 Ok(())
188 }
189}
190
191/// A path string that does not describe an address.
192#[derive(Debug, Clone, PartialEq, Eq, Error)]
193pub enum GffPathParseError {
194 /// The text between separators was empty.
195 #[error("empty path segment in `{path}`")]
196 EmptySegment {
197 /// The path as supplied.
198 path: String,
199 },
200 /// A `[...]` did not hold a number.
201 #[error("`{index}` in `{path}` is not a list index")]
202 BadIndex {
203 /// The path as supplied.
204 path: String,
205 /// The text found between the brackets.
206 index: String,
207 },
208 /// A `[` was never closed.
209 #[error("unclosed `[` in `{path}`")]
210 UnclosedIndex {
211 /// The path as supplied.
212 path: String,
213 },
214 /// A segment is not usable as a GFF label.
215 #[error("`{segment}` in `{path}` is not a valid label: {source}")]
216 BadLabel {
217 /// The path as supplied.
218 path: String,
219 /// The offending segment.
220 segment: String,
221 /// Why the label was rejected.
222 source: GffLabelError,
223 },
224}
225
226impl FromStr for GffPath {
227 type Err = GffPathParseError;
228
229 /// Parses dot notation with bracketed list indices.
230 ///
231 /// `PropertiesList[0].PropertyName` is three segments: a field, an index,
232 /// and a field.
233 fn from_str(text: &str) -> Result<Self, Self::Err> {
234 let path = || text.to_owned();
235 let mut segments = Vec::new();
236
237 for part in text.split('.') {
238 let (name, mut rest) = match part.find('[') {
239 Some(at) => (&part[..at], &part[at..]),
240 None => (part, ""),
241 };
242 if name.is_empty() {
243 return Err(GffPathParseError::EmptySegment { path: path() });
244 }
245 segments.push(GffPathSegment::Field(GffLabel::new(name).map_err(
246 |source| GffPathParseError::BadLabel {
247 path: path(),
248 segment: name.to_owned(),
249 source,
250 },
251 )?));
252
253 while !rest.is_empty() {
254 let end = rest
255 .find(']')
256 .ok_or_else(|| GffPathParseError::UnclosedIndex { path: path() })?;
257 let digits = &rest[1..end];
258 let index = digits
259 .parse::<usize>()
260 .map_err(|_| GffPathParseError::BadIndex {
261 path: path(),
262 index: digits.to_owned(),
263 })?;
264 segments.push(GffPathSegment::Index(index));
265 rest = &rest[end + 1..];
266 }
267 }
268
269 Ok(Self { segments })
270 }
271}
272
273/// A path that does not resolve, or an edit the tree will not take.
274///
275/// A `Result` rather than a hit-plus-diagnostics pair. A tier walk can half
276/// succeed, answering from a lower tier while a higher one was unreadable, and
277/// both halves matter. Addressing a path cannot: it arrives or it does not, for
278/// one reason.
279#[derive(Debug, Clone, PartialEq, Eq, Error)]
280pub enum GffPathError {
281 /// A path with no segments names nothing to act on.
282 #[error("an empty path names no field")]
283 EmptyPath,
284 /// A path started by indexing, with no list yet in hand.
285 #[error("`{path}` starts with a list index, but the root is a struct")]
286 IndexAtRoot {
287 /// The path as supplied.
288 path: String,
289 },
290 /// The struct has no field under that label.
291 #[error("no field `{label}` at `{at}`")]
292 NoSuchField {
293 /// Where the walk had reached.
294 at: String,
295 /// The label looked for.
296 label: String,
297 },
298 /// The struct carries that label more than once.
299 ///
300 /// Which copy the caller meant is not recoverable from the path, and the
301 /// copies are not known to agree on anything but their value today.
302 #[error("`{at}` carries `{label}` {count} times, so the path names no single field")]
303 AmbiguousLabel {
304 /// Where the walk had reached.
305 at: String,
306 /// The label looked for.
307 label: String,
308 /// How many fields carry it.
309 count: usize,
310 },
311 /// The list is shorter than the index.
312 #[error("`{at}` has {len} element(s), so index {index} is out of range")]
313 IndexOutOfRange {
314 /// Where the walk had reached.
315 at: String,
316 /// The index asked for.
317 index: usize,
318 /// How many elements the list holds.
319 len: usize,
320 },
321 /// A field was indexed and is not a list.
322 #[error("`{at}` is {actual}, which cannot be indexed")]
323 NotAList {
324 /// Where the walk had reached.
325 at: String,
326 /// The type actually found.
327 actual: &'static str,
328 },
329 /// A field was descended into and is not a struct.
330 #[error("`{at}` is {actual}, which has no fields")]
331 NotAStruct {
332 /// Where the walk had reached.
333 at: String,
334 /// The type actually found.
335 actual: &'static str,
336 },
337 /// The replacement is a different type from what the field holds.
338 ///
339 /// The existing field's type is the only oracle available here. The
340 /// per-format schemas that could answer "what *should* this label be" live
341 /// in `rakata-generics`, above this crate, and asking that question is
342 /// validation, which belongs to lint.
343 #[error("`{at}` holds {existing} and the replacement is {replacement}; use `insert` after `remove` to change a field's type deliberately")]
344 TypeMismatch {
345 /// The field being replaced.
346 at: String,
347 /// The type currently stored.
348 existing: &'static str,
349 /// The type offered.
350 replacement: &'static str,
351 },
352 /// `insert` was given a label the struct already carries.
353 #[error("`{at}` already exists; `set` replaces a field and `insert` adds one")]
354 AlreadyExists {
355 /// The field that is already present.
356 at: String,
357 },
358 /// A field operation ended at a list index, or the reverse.
359 #[error("`{operation}` needs a path ending in {expected}, and `{at}` does not")]
360 WrongSegmentKind {
361 /// The path as supplied.
362 at: String,
363 /// The operation that could not use it.
364 operation: &'static str,
365 /// The kind of last segment the operation needs.
366 expected: &'static str,
367 },
368}
369
370/// Where a partial walk of a path has arrived.
371enum Cursor<'a> {
372 Struct(&'a mut GffStruct),
373 Value(&'a mut GffValue),
374}
375
376impl GffStruct {
377 /// Reads the value a path names.
378 ///
379 /// The read counterpart to [`Self::set`], and the way to do a
380 /// read-modify-write over a large file without building a typed view per
381 /// field.
382 ///
383 /// # Errors
384 ///
385 /// Every operation here walks the path the same way and can fail the same
386 /// way partway along it: [`GffPathError::EmptyPath`] for a path with no
387 /// segments, [`GffPathError::IndexAtRoot`] for one starting with an index,
388 /// [`GffPathError::NotAStruct`] or [`GffPathError::NotAList`] when a
389 /// segment's value is not the kind the next segment needs,
390 /// [`GffPathError::IndexOutOfRange`] when a list is shorter than the index,
391 /// [`GffPathError::NoSuchField`] when a struct does not carry the label,
392 /// and [`GffPathError::AmbiguousLabel`] when it carries it more than once.
393 /// That last one is a refusal rather than a first match; see the module
394 /// docs.
395 ///
396 /// On top of those, [`GffPathError::WrongSegmentKind`] when `path` ends in
397 /// an index. That lands on a bare [`GffStruct`], which is not a
398 /// [`GffValue`]; [`Self::struct_id`] is the call that takes such a path.
399 pub fn get(&self, path: &GffPath) -> Result<&GffValue, GffPathError> {
400 let segments = path.segments();
401 if segments.is_empty() {
402 return Err(GffPathError::EmptyPath);
403 }
404
405 let mut current: Option<&GffValue> = None;
406 let mut structure: &GffStruct = self;
407
408 for (depth, segment) in segments.iter().enumerate() {
409 match segment {
410 GffPathSegment::Field(label) => {
411 if let Some(value) = current {
412 structure = as_struct(value, path, depth)?;
413 }
414 let at = sole_position(&structure.fields, label, path, depth)?;
415 current = Some(&structure.fields[at].value);
416 }
417 GffPathSegment::Index(index) => {
418 let Some(value) = current else {
419 return Err(GffPathError::IndexAtRoot {
420 path: path.to_string(),
421 });
422 };
423 let list = as_list(value, path, depth)?;
424 structure = list
425 .get(*index)
426 .ok_or_else(|| GffPathError::IndexOutOfRange {
427 at: path.prefix(depth),
428 index: *index,
429 len: list.len(),
430 })?;
431 current = None;
432 }
433 }
434 }
435
436 // A path ending in an index names a struct, which is not a field value.
437 current.ok_or_else(|| GffPathError::WrongSegmentKind {
438 at: path.to_string(),
439 operation: "get",
440 expected: "a field label",
441 })
442 }
443
444 /// Replaces the value a path names, returning what was there.
445 ///
446 /// Refuses a label the struct does not already carry, and refuses a
447 /// replacement of a different type from the one stored. Both refusals exist
448 /// because the alternative is a file that looks edited and is not: an
449 /// appended duplicate loses to the original on lookup, and a widened
450 /// integer shifts every byte after it.
451 ///
452 /// To change a field's type on purpose, [`remove`](Self::remove) it and
453 /// [`insert`](Self::insert) it back.
454 ///
455 /// # Errors
456 ///
457 /// The walk failures [`Self::get`] lists, plus
458 /// [`GffPathError::TypeMismatch`] when `value` is not the type the field
459 /// already holds. [`GffPathError::NoSuchField`] is the ordinary way a
460 /// mistyped label arrives here rather than becoming a new field.
461 pub fn set(&mut self, path: &GffPath, value: GffValue) -> Result<GffValue, GffPathError> {
462 let depth = path.segments().len() - 1;
463 let (structure, label) = self.field_parent_mut(path, "set")?;
464 let at = sole_position(&structure.fields, &label, path, depth)?;
465 let existing = &mut structure.fields[at];
466
467 let stored = gff_value_type(&existing.value);
468 let offered = gff_value_type(&value);
469 if stored != offered {
470 return Err(GffPathError::TypeMismatch {
471 at: path.to_string(),
472 existing: stored.name(),
473 replacement: offered.name(),
474 });
475 }
476
477 Ok(std::mem::replace(&mut existing.value, value))
478 }
479
480 /// Adds a field a path names, which must not already be there.
481 ///
482 /// The value's own type decides the field's type, there being nothing to
483 /// preserve.
484 ///
485 /// On disk a struct holding one field stores that field's index directly
486 /// and a struct holding more stores an offset into the shared index array,
487 /// so adding a second field to a one-field struct changes the struct's
488 /// encoded shape. Nothing here has to arrange that: the writer picks the
489 /// shape from the field count when it encodes.
490 ///
491 /// # Errors
492 ///
493 /// The walk failures [`Self::get`] lists for the path's parent, plus
494 /// [`GffPathError::AlreadyExists`] when the struct already carries that
495 /// label. Adding a second copy is refused rather than done, because the
496 /// original would go on winning every lookup.
497 pub fn insert(&mut self, path: &GffPath, value: GffValue) -> Result<(), GffPathError> {
498 self.insert_at_position(path, value, usize::MAX)
499 }
500
501 /// Adds a field among its siblings rather than after them.
502 ///
503 /// `position` is clamped to the end, so `usize::MAX` appends and a position
504 /// from a struct that has since lost fields still lands somewhere valid.
505 ///
506 /// This exists because putting a field back is not the same operation as
507 /// adding one. A GFF struct stores its fields in order and the writer emits
508 /// them in that order, so restoring a removed field by appending produces a
509 /// file that differs from the original everywhere after the hole, which is
510 /// what an undo of a deletion did before.
511 ///
512 /// # Errors
513 ///
514 /// The same as [`Self::insert`]. `position` is clamped rather than
515 /// checked, so it is never the reason this fails.
516 pub fn insert_at_position(
517 &mut self,
518 path: &GffPath,
519 value: GffValue,
520 position: usize,
521 ) -> Result<(), GffPathError> {
522 let (structure, label) = self.field_parent_mut(path, "insert")?;
523 if structure.fields.iter().any(|field| field.label == label) {
524 return Err(GffPathError::AlreadyExists {
525 at: path.to_string(),
526 });
527 }
528 let position = position.min(structure.fields.len());
529 structure.fields.insert(position, GffField { label, value });
530 Ok(())
531 }
532
533 /// Where the field `path` names sits among its siblings.
534 ///
535 /// `None` when the parent does not resolve, the path does not end in a
536 /// label, or no such field is there.
537 pub fn position(&self, path: &GffPath) -> Option<usize> {
538 let segments = path.segments();
539 let GffPathSegment::Field(label) = segments.last()? else {
540 return None;
541 };
542 let structure = if segments.len() == 1 {
543 // A one-segment path names a field of this struct itself, and
544 // there is no parent address to resolve.
545 self
546 } else {
547 let parent = GffPath::new(segments[..segments.len() - 1].to_vec());
548 match self.get(&parent) {
549 Ok(GffValue::Struct(nested)) => nested.as_ref(),
550 _ => return None,
551 }
552 };
553 structure
554 .fields
555 .iter()
556 .position(|field| field.label == *label)
557 }
558
559 /// Removes what a path names, returning it.
560 ///
561 /// A path ending in a label removes that field. A path ending in an index
562 /// removes that list element and returns it wrapped in a
563 /// [`GffValue::Struct`].
564 ///
565 /// Removing an element does not renumber its siblings. No list in the
566 /// engine reads an element's struct id as a position, so the ids that
567 /// remain still mean what they meant.
568 ///
569 /// # Errors
570 ///
571 /// The walk failures [`Self::get`] lists. Nothing is removed when any of
572 /// them fires, so a failed remove leaves the tree as it was.
573 pub fn remove(&mut self, path: &GffPath) -> Result<GffValue, GffPathError> {
574 let segments = path.segments();
575 let Some(last) = segments.last() else {
576 return Err(GffPathError::EmptyPath);
577 };
578
579 match last {
580 GffPathSegment::Field(label) => {
581 let depth = segments.len() - 1;
582 let structure = self.walk_to_struct(path, depth)?;
583 let at = sole_position(&structure.fields, label, path, depth)?;
584 Ok(structure.fields.remove(at).value)
585 }
586 GffPathSegment::Index(index) => {
587 let list = self.walk_to_list(path, segments.len() - 1)?;
588 if *index >= list.len() {
589 return Err(GffPathError::IndexOutOfRange {
590 at: path.prefix(segments.len() - 1),
591 index: *index,
592 len: list.len(),
593 });
594 }
595 Ok(GffValue::Struct(Box::new(list.remove(*index))))
596 }
597 }
598 }
599
600 /// Appends `element` to the list a path names, returning its index.
601 ///
602 /// The element carries its own `struct_id`, and choosing it is the caller's
603 /// job. Several lists have the engine compare that id against a per-list
604 /// constant and silently skip an element that does not match, and which
605 /// constant belongs to which list is per-format knowledge that lives above
606 /// this crate.
607 ///
608 /// # Errors
609 ///
610 /// The walk failures [`Self::get`] lists, with
611 /// [`GffPathError::NotAList`] the one that means `path` named a field
612 /// that is not a list. The element's `struct_id` is not checked against
613 /// anything, since nothing here knows which list wants which id.
614 pub fn push_element(
615 &mut self,
616 path: &GffPath,
617 element: GffStruct,
618 ) -> Result<usize, GffPathError> {
619 let list = self.walk_to_list(path, path.segments().len())?;
620 list.push(element);
621 Ok(list.len() - 1)
622 }
623
624 /// Reads the struct id of the list element a path names.
625 ///
626 /// An element's id is not a field and not a [`GffValue`], so [`get`] cannot
627 /// reach it: a path ending in an index lands on a bare [`GffStruct`]. A
628 /// nested struct *field* needs nothing special, since `get` hands back the
629 /// [`GffValue::Struct`] and the id is on the struct inside it.
630 ///
631 /// Some formats carry meaning there. A UTC's equipment slot **is** its
632 /// `Equip_ItemList` element's struct id and appears in no field, so reading
633 /// which slot an item occupies is exactly this call.
634 ///
635 /// [`get`]: Self::get
636 ///
637 /// # Errors
638 ///
639 /// The walk failures [`Self::get`] lists, plus
640 /// [`GffPathError::WrongSegmentKind`] when `path` ends in a label rather
641 /// than an index. That is the mirror of `get`'s refusal: this call takes
642 /// exactly the paths that one will not.
643 pub fn struct_id(&self, path: &GffPath) -> Result<i32, GffPathError> {
644 let (list, index) = self.element_parent(path)?;
645 list.get(index)
646 .map(|element| element.struct_id)
647 .ok_or_else(|| GffPathError::IndexOutOfRange {
648 at: path.prefix(path.segments().len() - 1),
649 index,
650 len: list.len(),
651 })
652 }
653
654 /// Sets the struct id of the list element a path names, returning the old.
655 ///
656 /// The counterpart to [`struct_id`](Self::struct_id), and the way to move a
657 /// UTC equipment item between slots: the slot is the id, so re-equipping is
658 /// a change to an existing element rather than a removal and an append.
659 ///
660 /// Which ids a list accepts is the caller's to know. Several lists have the
661 /// engine compare the id against a per-list constant and silently skip an
662 /// element that does not match, and that mapping is per-format knowledge
663 /// living above this crate.
664 ///
665 /// # Errors
666 ///
667 /// The same as [`Self::struct_id`]. `struct_id` itself is not validated,
668 /// so an id no list accepts is written without complaint.
669 pub fn set_struct_id(&mut self, path: &GffPath, struct_id: i32) -> Result<i32, GffPathError> {
670 let index = self.element_index(path)?;
671 let at = path.prefix(path.segments().len() - 1);
672 let list = self.walk_to_list(path, path.segments().len() - 1)?;
673 let len = list.len();
674 let element =
675 list.get_mut(index)
676 .ok_or(GffPathError::IndexOutOfRange { at, index, len })?;
677 Ok(std::mem::replace(&mut element.struct_id, struct_id))
678 }
679
680 /// The index a struct-id path ends with, refusing one that ends elsewhere.
681 fn element_index(&self, path: &GffPath) -> Result<usize, GffPathError> {
682 let segments = path.segments();
683 let Some(last) = segments.last() else {
684 return Err(GffPathError::EmptyPath);
685 };
686 match last {
687 GffPathSegment::Index(index) => Ok(*index),
688 GffPathSegment::Field(_) => Err(GffPathError::WrongSegmentKind {
689 at: path.to_string(),
690 operation: "struct_id",
691 expected: "a list index",
692 }),
693 }
694 }
695
696 /// Resolves a struct-id path to the list holding the element, and its index.
697 fn element_parent(&self, path: &GffPath) -> Result<(&Vec<GffStruct>, usize), GffPathError> {
698 let index = self.element_index(path)?;
699 let segments = path.segments();
700
701 // A path that is only an index has no list in front of it, which the
702 // parent walk below would report as an empty path rather than as the
703 // root not being a list.
704 if segments.len() == 1 {
705 return Err(GffPathError::IndexAtRoot {
706 path: path.to_string(),
707 });
708 }
709
710 let parent = GffPath::new(segments[..segments.len() - 1].to_vec());
711 match self.get(&parent)? {
712 GffValue::List(list) => Ok((list, index)),
713 other => Err(GffPathError::NotAList {
714 at: parent.to_string(),
715 actual: gff_value_type(other).name(),
716 }),
717 }
718 }
719
720 /// Splits a path into the struct holding its last field and that label.
721 fn field_parent_mut(
722 &mut self,
723 path: &GffPath,
724 operation: &'static str,
725 ) -> Result<(&mut GffStruct, GffLabel), GffPathError> {
726 let segments = path.segments();
727 let Some(last) = segments.last() else {
728 return Err(GffPathError::EmptyPath);
729 };
730 let GffPathSegment::Field(label) = last else {
731 return Err(GffPathError::WrongSegmentKind {
732 at: path.to_string(),
733 operation,
734 expected: "a field label",
735 });
736 };
737 let label = *label;
738 Ok((self.walk_to_struct(path, segments.len() - 1)?, label))
739 }
740
741 /// Walks the first `depth` segments and requires a struct at the end.
742 fn walk_to_struct(
743 &mut self,
744 path: &GffPath,
745 depth: usize,
746 ) -> Result<&mut GffStruct, GffPathError> {
747 match self.descend_to(path, depth)? {
748 Cursor::Struct(structure) => Ok(structure),
749 Cursor::Value(value) => {
750 let actual = gff_value_type(value).name();
751 match value {
752 GffValue::Struct(inner) => Ok(inner.as_mut()),
753 _ => Err(GffPathError::NotAStruct {
754 at: path.prefix(depth),
755 actual,
756 }),
757 }
758 }
759 }
760 }
761
762 /// Walks the first `depth` segments and requires a list at the end.
763 fn walk_to_list(
764 &mut self,
765 path: &GffPath,
766 depth: usize,
767 ) -> Result<&mut Vec<GffStruct>, GffPathError> {
768 match self.descend_to(path, depth)? {
769 Cursor::Struct(_) => Err(GffPathError::NotAList {
770 at: path.prefix(depth),
771 actual: "a struct",
772 }),
773 Cursor::Value(value) => {
774 let actual = gff_value_type(value).name();
775 match value {
776 GffValue::List(list) => Ok(list),
777 _ => Err(GffPathError::NotAList {
778 at: path.prefix(depth),
779 actual,
780 }),
781 }
782 }
783 }
784 }
785
786 /// Descends the first `depth` segments of `path`.
787 ///
788 /// Not to be confused with [`walk`](Self::walk), which is the public
789 /// traversal of a whole tree. This positions a cursor for one edit.
790 fn descend_to(&mut self, path: &GffPath, depth: usize) -> Result<Cursor<'_>, GffPathError> {
791 let mut cursor = Cursor::Struct(self);
792
793 for (at, segment) in path.segments().iter().take(depth).enumerate() {
794 cursor = match segment {
795 GffPathSegment::Field(label) => {
796 let structure = match cursor {
797 Cursor::Struct(structure) => structure,
798 Cursor::Value(value) => {
799 let actual = gff_value_type(value).name();
800 match value {
801 GffValue::Struct(inner) => inner.as_mut(),
802 _ => {
803 return Err(GffPathError::NotAStruct {
804 at: path.prefix(at),
805 actual,
806 })
807 }
808 }
809 }
810 };
811 let found = sole_position(&structure.fields, label, path, at)?;
812 Cursor::Value(&mut structure.fields[found].value)
813 }
814 GffPathSegment::Index(index) => {
815 let Cursor::Value(value) = cursor else {
816 return Err(GffPathError::IndexAtRoot {
817 path: path.to_string(),
818 });
819 };
820 let actual = gff_value_type(value).name();
821 let GffValue::List(list) = value else {
822 return Err(GffPathError::NotAList {
823 at: path.prefix(at),
824 actual,
825 });
826 };
827 let len = list.len();
828 Cursor::Struct(list.get_mut(*index).ok_or(GffPathError::IndexOutOfRange {
829 at: path.prefix(at),
830 index: *index,
831 len,
832 })?)
833 }
834 };
835 }
836
837 Ok(cursor)
838 }
839}
840
841/// The one position `label` occupies, refusing both zero copies and several.
842///
843/// Every field lookup in this module goes through here, so an ambiguous label
844/// fails wherever it appears in a path rather than only in its last segment.
845fn sole_position(
846 fields: &[GffField],
847 label: &GffLabel,
848 path: &GffPath,
849 depth: usize,
850) -> Result<usize, GffPathError> {
851 let mut carried = fields
852 .iter()
853 .enumerate()
854 .filter(|(_, field)| field.label == *label)
855 .map(|(index, _)| index);
856
857 let first = carried.next().ok_or_else(|| GffPathError::NoSuchField {
858 at: path.prefix(depth),
859 label: label.to_string(),
860 })?;
861
862 let count = 1 + carried.count();
863 if count > 1 {
864 return Err(GffPathError::AmbiguousLabel {
865 at: path.prefix(depth),
866 label: label.to_string(),
867 count,
868 });
869 }
870 Ok(first)
871}
872
873/// Borrows a value as the struct it must be to be descended into.
874fn as_struct<'a>(
875 value: &'a GffValue,
876 path: &GffPath,
877 depth: usize,
878) -> Result<&'a GffStruct, GffPathError> {
879 match value {
880 GffValue::Struct(inner) => Ok(inner.as_ref()),
881 other => Err(GffPathError::NotAStruct {
882 at: path.prefix(depth),
883 actual: gff_value_type(other).name(),
884 }),
885 }
886}
887
888/// Borrows a value as the list it must be to be indexed.
889fn as_list<'a>(
890 value: &'a GffValue,
891 path: &GffPath,
892 depth: usize,
893) -> Result<&'a Vec<GffStruct>, GffPathError> {
894 match value {
895 GffValue::List(list) => Ok(list),
896 other => Err(GffPathError::NotAList {
897 at: path.prefix(depth),
898 actual: gff_value_type(other).name(),
899 }),
900 }
901}