1use std::ops::Range;
2use std::path::{Path, PathBuf};
3
4use toml_edit::{Array, ArrayOfTables, DocumentMut, InlineTable, Item, Table, TableLike, Value};
5
6use crate::edit::Edit;
7use crate::error::DocError;
8use crate::files::Files;
9use crate::splice::diff;
10use crate::stamp::Stamp;
11
12pub const LABEL_KEY: &str = "name";
13
14#[derive(Clone, Debug, PartialEq, Eq, Hash)]
15pub enum Key {
16 Table(String),
17 Object(String),
18 Index(usize),
19}
20
21#[derive(Clone, Debug)]
22pub enum Op {
23 Set { path: Vec<Key>, value: Item },
24 Unset { path: Vec<Key> },
25 Push { path: Vec<Key>, value: Item },
26 Insert { path: Vec<Key>, value: Item },
27}
28
29impl Op {
30 pub fn path(&self) -> &[Key] {
31 match self {
32 Op::Set { path, .. }
33 | Op::Unset { path }
34 | Op::Push { path, .. }
35 | Op::Insert { path, .. } => path,
36 }
37 }
38
39 pub fn verb(&self) -> &'static str {
40 match self {
41 Op::Set { .. } => "set",
42 Op::Unset { .. } => "unset",
43 Op::Push { .. } => "push",
44 Op::Insert { .. } => "insert",
45 }
46 }
47}
48
49fn bare(key: &str) -> bool {
50 !key.is_empty()
51 && key
52 .bytes()
53 .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
54}
55
56pub fn path_text(path: &[Key]) -> String {
57 let mut out = String::new();
58 for key in path {
59 match key {
60 Key::Table(name) => {
61 if !out.is_empty() {
62 out.push('.');
63 }
64 if bare(name) {
65 out.push_str(name);
66 } else {
67 out.push('"');
68 for c in name.chars() {
69 if c == '"' || c == '\\' {
70 out.push('\\');
71 }
72 out.push(c);
73 }
74 out.push('"');
75 }
76 }
77 Key::Object(name) => {
78 out.push('[');
79 out.push_str(name);
80 out.push(']');
81 }
82 Key::Index(i) => {
83 out.push('[');
84 out.push_str(&i.to_string());
85 out.push(']');
86 }
87 }
88 }
89 out
90}
91
92pub fn parse_path(text: &str) -> Result<Vec<Key>, String> {
93 let mut keys = Vec::new();
94 let mut chars = text.chars().peekable();
95 let mut expect_key = true;
96 while let Some(&c) = chars.peek() {
97 match c {
98 '[' => {
99 chars.next();
100 let mut inner = String::new();
101 loop {
102 match chars.next() {
103 Some(']') => break,
104 Some(c) => inner.push(c),
105 None => return Err(format!("`{text}` has an unclosed `[`")),
106 }
107 }
108 if inner.is_empty() {
109 return Err(format!("`{text}` has an empty `[]`"));
110 }
111 keys.push(if inner.bytes().all(|b| b.is_ascii_digit()) {
112 Key::Index(
113 inner
114 .parse()
115 .map_err(|_| format!("`{inner}` is too large an index"))?,
116 )
117 } else {
118 Key::Object(inner)
119 });
120 expect_key = false;
121 }
122 '.' if !expect_key => {
123 chars.next();
124 expect_key = true;
125 }
126 '"' if expect_key => {
127 chars.next();
128 let mut name = String::new();
129 loop {
130 match chars.next() {
131 Some('"') => break,
132 Some('\\') => match chars.next() {
133 Some(c) => name.push(c),
134 None => return Err(format!("`{text}` ends inside a quoted key")),
135 },
136 Some(c) => name.push(c),
137 None => return Err(format!("`{text}` has an unclosed quote")),
138 }
139 }
140 keys.push(Key::Table(name));
141 expect_key = false;
142 }
143 _ if expect_key => {
144 let mut name = String::new();
145 while let Some(&c) = chars.peek() {
146 if c == '.' || c == '[' {
147 break;
148 }
149 name.push(c);
150 chars.next();
151 }
152 if !bare(&name) {
153 return Err(format!("`{name}` is not a bare key; quote it"));
154 }
155 keys.push(Key::Table(name));
156 expect_key = false;
157 }
158 _ => return Err(format!("`{text}` has `{c}` where a `.` or `[` belongs")),
159 }
160 }
161 if keys.is_empty() || (expect_key && !text.is_empty()) {
162 return Err(format!("`{text}` is not a path"));
163 }
164 Ok(keys)
165}
166
167enum At<'a> {
168 Table(&'a dyn TableLike),
169 Tables(&'a ArrayOfTables),
170 Value(&'a Value),
171}
172
173fn at_item(item: &Item) -> Option<At<'_>> {
174 match item {
175 Item::Table(t) => Some(At::Table(t)),
176 Item::ArrayOfTables(a) => Some(At::Tables(a)),
177 Item::Value(Value::InlineTable(t)) => Some(At::Table(t)),
178 Item::Value(v) => Some(At::Value(v)),
179 Item::None => None,
180 }
181}
182
183fn label_of(t: &dyn TableLike) -> Option<&str> {
184 t.get(LABEL_KEY).and_then(Item::as_str)
185}
186
187fn inline_label(v: &Value) -> Option<&str> {
188 v.as_inline_table()
189 .and_then(|t| t.get(LABEL_KEY))
190 .and_then(Value::as_str)
191}
192
193fn step<'a>(at: At<'a>, key: &Key) -> Option<At<'a>> {
194 match (at, key) {
195 (At::Table(t), Key::Table(k)) => t.get(k).and_then(at_item),
196 (At::Tables(a), Key::Object(n) | Key::Table(n)) => a
197 .iter()
198 .find(|t| label_of(*t) == Some(n.as_str()))
199 .map(|t| At::Table(t)),
200 (At::Tables(a), Key::Index(i)) => a.get(*i).map(|t| At::Table(t)),
201 (At::Value(Value::Array(a)), Key::Index(i)) => a.get(*i).and_then(value_at_item),
202 (At::Value(Value::Array(a)), Key::Object(n)) => a
203 .iter()
204 .find(|v| inline_label(v) == Some(n.as_str()))
205 .and_then(value_at_item),
206 _ => None,
207 }
208}
209
210fn value_at_item(v: &Value) -> Option<At<'_>> {
211 match v {
212 Value::InlineTable(t) => Some(At::Table(t)),
213 v => Some(At::Value(v)),
214 }
215}
216
217pub fn exists(doc: &DocumentMut, path: &[Key]) -> bool {
218 let mut at = At::Table(doc.as_table());
219 for key in path {
220 match step(at, key) {
221 Some(next) => at = next,
222 None => return false,
223 }
224 }
225 true
226}
227
228pub fn value_at(doc: &DocumentMut, path: &[Key]) -> Option<Value> {
229 let mut at = At::Table(doc.as_table());
230 for key in path {
231 at = step(at, key)?;
232 }
233 match at {
234 At::Value(v) => Some(v.clone()),
235 At::Table(t) => {
236 let mut inline = InlineTable::new();
237 for (k, item) in t.iter() {
238 if let Some(v) = item.as_value() {
239 inline.insert(k, v.clone());
240 }
241 }
242 Some(Value::InlineTable(inline))
243 }
244 At::Tables(_) => None,
245 }
246}
247
248fn len_at(doc: &DocumentMut, path: &[Key]) -> Result<Option<usize>, String> {
249 let mut at = At::Table(doc.as_table());
250 for (n, key) in path.iter().enumerate() {
251 match step(at, key) {
252 Some(next) => at = next,
253 None if n + 1 == path.len() => return Ok(None),
254 None => return Err(format!("nothing at `{}`", path_text(&path[..=n]))),
255 }
256 }
257 match at {
258 At::Tables(a) => Ok(Some(a.len())),
259 At::Value(Value::Array(a)) => Ok(Some(a.len())),
260 _ => Err(format!("`{}` is not an array", path_text(path))),
261 }
262}
263
264enum Action {
265 Set(Item),
266 Unset,
267 Insert(Item),
268}
269
270struct Done {
271 old: Option<Item>,
272 slot: Key,
273}
274
275fn fresh(value: &Value) -> bool {
276 value.decor().prefix().is_none() && value.decor().suffix().is_none()
277}
278
279fn keep_look(old: &Item, new: &mut Item) {
280 match (old, new) {
281 (Item::Value(old), Item::Value(new)) if fresh(new) => {
282 *new.decor_mut() = old.decor().clone();
283 }
284 (Item::Table(old), Item::Table(new)) if new.position().is_none() => {
285 new.set_position(old.position());
286 *new.decor_mut() = old.decor().clone();
287 }
288 _ => {}
289 }
290}
291
292fn into_value(item: Item) -> Result<Value, String> {
293 item.into_value()
294 .map_err(|_| "expected a value here, not a table".to_string())
295}
296
297fn edit_item(item: &mut Item, path: &[Key], action: Action) -> Result<Done, String> {
298 match item {
299 Item::Table(t) => edit_table(t, false, path, action),
300 Item::ArrayOfTables(a) => edit_tables(a, path, action),
301 Item::Value(Value::InlineTable(t)) => edit_table(t, true, path, action),
302 Item::Value(Value::Array(a)) => edit_array(a, path, action),
303 Item::Value(_) => Err(format!("`{}` has nothing inside it", path_text(&path[..1]))),
304 Item::None => Err("nothing is there".to_string()),
305 }
306}
307
308fn edit_table(
309 t: &mut dyn TableLike,
310 inline: bool,
311 path: &[Key],
312 action: Action,
313) -> Result<Done, String> {
314 let Key::Table(key) = &path[0] else {
315 return Err(format!(
316 "`{}` is not a key; a table is addressed by key name",
317 path_text(&path[..1])
318 ));
319 };
320 if path.len() > 1 {
321 let child = t.get_mut(key).ok_or_else(|| format!("no key `{key}`"))?;
322 return edit_item(child, &path[1..], action);
323 }
324 let slot = Key::Table(key.clone());
325 match action {
326 Action::Set(mut new) => {
327 if inline {
328 new = Item::Value(into_value(new)?);
329 }
330 match t.get_mut(key) {
331 Some(old_slot) => {
332 keep_look(old_slot, &mut new);
333 let old = std::mem::replace(old_slot, new);
334 Ok(Done {
335 old: Some(old),
336 slot,
337 })
338 }
339 None => {
340 t.insert(key, new);
341 Ok(Done { old: None, slot })
342 }
343 }
344 }
345 Action::Unset => match t.remove(key) {
346 Some(old) => Ok(Done {
347 old: Some(old),
348 slot,
349 }),
350 None => Err(format!("no key `{key}` to unset")),
351 },
352 Action::Insert(_) => Err(format!(
353 "`{key}` is a key; insert takes a position in an array"
354 )),
355 }
356}
357
358fn find_table(a: &ArrayOfTables, name: &str) -> Option<usize> {
359 a.iter().position(|t| label_of(t) == Some(name))
360}
361
362fn table_of(item: Item) -> Result<Table, String> {
363 match item {
364 Item::Table(t) => Ok(t),
365 Item::Value(Value::InlineTable(t)) => Ok(t.into_table()),
366 _ => Err("expected a table".to_string()),
367 }
368}
369
370fn edit_tables(a: &mut ArrayOfTables, path: &[Key], action: Action) -> Result<Done, String> {
371 let len = a.len();
372 let found = match &path[0] {
373 Key::Index(i) => Some(*i),
374 Key::Object(n) | Key::Table(n) => find_table(a, n),
375 };
376 if path.len() > 1 {
377 let at = found.ok_or_else(|| format!("no entry `{}`", path_text(&path[..1])))?;
378 let table = a
379 .get_mut(at)
380 .ok_or_else(|| format!("no entry {at}, the array has {len}"))?;
381 return edit_table(table, false, &path[1..], action);
382 }
383 match (action, found) {
384 (Action::Set(item), Some(at)) if at < len => {
385 let mut new = Item::Table(table_of(item)?);
386 let old = Item::Table(a.get(at).cloned().expect("in range"));
387 keep_look(&old, &mut new);
388 a.replace(at, table_of(new)?);
389 Ok(Done {
390 old: Some(old),
391 slot: Key::Index(at),
392 })
393 }
394 (Action::Set(item), at) if at.is_none_or(|at| at == len) => {
395 a.push(table_of(item)?);
396 Ok(Done {
397 old: None,
398 slot: Key::Index(len),
399 })
400 }
401 (Action::Set(_), _) => Err(format!(
402 "no room at `{}`, the array has {len}",
403 path_text(&path[..1])
404 )),
405 (Action::Unset, Some(at)) if at < len => Ok(Done {
406 old: Some(Item::Table(a.remove(at))),
407 slot: Key::Index(at),
408 }),
409 (Action::Unset, _) => Err(format!("no entry `{}` to remove", path_text(&path[..1]))),
410 (Action::Insert(item), Some(at)) if at <= len && matches!(path[0], Key::Index(_)) => {
411 a.insert(at, table_of(item)?);
412 Ok(Done {
413 old: None,
414 slot: Key::Index(at),
415 })
416 }
417 (Action::Insert(_), _) => Err(format!(
418 "insert takes a position up to {len}, not `{}`",
419 path_text(&path[..1])
420 )),
421 }
422}
423
424fn find_value(a: &Array, key: &Key) -> Option<usize> {
425 match key {
426 Key::Index(i) => Some(*i),
427 Key::Object(n) => a.iter().position(|v| inline_label(v) == Some(n.as_str())),
428 Key::Table(_) => None,
429 }
430}
431
432fn edit_array(a: &mut Array, path: &[Key], action: Action) -> Result<Done, String> {
433 let len = a.len();
434 if let Key::Table(name) = &path[0] {
435 return Err(format!(
436 "`{name}` is not a position; an array is addressed by index or by name"
437 ));
438 }
439 let found = find_value(a, &path[0]);
440 if path.len() > 1 {
441 let at = found.ok_or_else(|| format!("no element `{}`", path_text(&path[..1])))?;
442 let element = a
443 .get_mut(at)
444 .ok_or_else(|| format!("no element {at}, the array has {len}"))?;
445 return match element {
446 Value::InlineTable(t) => edit_table(t, true, &path[1..], action),
447 Value::Array(inner) => edit_array(inner, &path[1..], action),
448 _ => Err(format!("element {at} has nothing inside it")),
449 };
450 }
451 let put = |a: &mut Array, at: usize, mut value: Value| {
452 if fresh(&value) {
453 let near = a
454 .get(at)
455 .or_else(|| at.checked_sub(1).and_then(|i| a.get(i)));
456 let prefix = near
457 .and_then(|v| v.decor().prefix())
458 .and_then(|p| p.as_str())
459 .map(str::to_string);
460 match prefix {
461 Some(prefix) if prefix.contains('\n') => value.decor_mut().set_prefix(prefix),
462 _ if at > 0 => value.decor_mut().set_prefix(" "),
463 _ => {}
464 }
465 if at == 0
466 && let Some(next) = a.get_mut(0)
467 && next.decor().prefix().and_then(|p| p.as_str()) == Some("")
468 {
469 next.decor_mut().set_prefix(" ");
470 }
471 }
472 a.insert_formatted(at, value);
473 };
474 match (action, found) {
475 (Action::Set(item), Some(at)) if at < len => {
476 let old = a.replace(at, into_value(item)?);
477 Ok(Done {
478 old: Some(Item::Value(old)),
479 slot: Key::Index(at),
480 })
481 }
482 (Action::Set(item), at) if at.is_none_or(|at| at == len) => {
483 put(a, len, into_value(item)?);
484 Ok(Done {
485 old: None,
486 slot: Key::Index(len),
487 })
488 }
489 (Action::Set(_), _) => Err(format!(
490 "no element `{}`, the array has {len}",
491 path_text(&path[..1])
492 )),
493 (Action::Unset, Some(at)) if at < len => Ok(Done {
494 old: Some(Item::Value(a.remove(at))),
495 slot: Key::Index(at),
496 }),
497 (Action::Unset, _) => Err(format!(
498 "no element `{}` to remove, the array has {len}",
499 path_text(&path[..1])
500 )),
501 (Action::Insert(item), Some(at)) if at <= len && matches!(path[0], Key::Index(_)) => {
502 put(a, at, into_value(item)?);
503 Ok(Done {
504 old: None,
505 slot: Key::Index(at),
506 })
507 }
508 (Action::Insert(_), _) => Err(format!(
509 "insert takes a position up to {len}, not `{}`",
510 path_text(&path[..1])
511 )),
512 }
513}
514
515fn renamed(path: &mut [Key], value: &Item, old: &Item) {
516 let n = path.len();
517 if n < 2 || !matches!(&path[n - 1], Key::Table(k) if k == LABEL_KEY) {
518 return;
519 }
520 let (Some(new), Some(was)) = (value.as_str(), old.as_str()) else {
521 return;
522 };
523 if matches!(&path[n - 2], Key::Object(x) if x == was) {
524 path[n - 2] = Key::Object(new.to_string());
525 }
526}
527
528fn with_slot(path: &[Key], slot: Key) -> Vec<Key> {
529 let mut path = path.to_vec();
530 if let Some(last) = path.last_mut() {
531 *last = slot;
532 }
533 path
534}
535
536fn new_array(value: Item) -> Result<Item, String> {
537 match value {
538 Item::Table(t) => {
539 let mut list = ArrayOfTables::new();
540 list.push(t);
541 Ok(Item::ArrayOfTables(list))
542 }
543 Item::Value(v) => {
544 let mut array = Array::new();
545 array.push(v);
546 Ok(Item::Value(Value::Array(array)))
547 }
548 _ => Err("push takes a value or a table".to_string()),
549 }
550}
551
552pub fn edit(doc: &mut DocumentMut, op: &Op) -> Result<Op, String> {
553 if op.path().is_empty() {
554 return Err("the path is empty".to_string());
555 }
556 let root = doc.as_item_mut();
557 match op {
558 Op::Set { path, value } => {
559 let done = edit_item(root, path, Action::Set(value.clone()))?;
560 let mut back = with_slot(path, done.slot);
561 Ok(match done.old {
562 Some(old) => {
563 renamed(&mut back, value, &old);
564 Op::Set {
565 path: back,
566 value: old,
567 }
568 }
569 None => Op::Unset { path: back },
570 })
571 }
572 Op::Unset { path } => {
573 let done = edit_item(root, path, Action::Unset)?;
574 let old = done.old.ok_or("nothing was there")?;
575 Ok(match done.slot {
576 Key::Index(_) => Op::Insert {
577 path: with_slot(path, done.slot),
578 value: old,
579 },
580 slot => Op::Set {
581 path: with_slot(path, slot),
582 value: old,
583 },
584 })
585 }
586 Op::Insert { path, value } => {
587 let done = edit_item(root, path, Action::Insert(value.clone()))?;
588 Ok(Op::Unset {
589 path: with_slot(path, done.slot),
590 })
591 }
592 Op::Push { path, value } => match len_at(doc, path)? {
593 Some(len) => {
594 let mut at = path.clone();
595 at.push(Key::Index(len));
596 edit(
597 doc,
598 &Op::Insert {
599 path: at,
600 value: value.clone(),
601 },
602 )
603 }
604 None => edit(
605 doc,
606 &Op::Set {
607 path: path.clone(),
608 value: new_array(value.clone())?,
609 },
610 ),
611 },
612 }
613}
614
615#[derive(Clone, Debug, PartialEq, Eq)]
616pub struct Replaced {
617 pub range: Range<usize>,
618 pub old: String,
619 pub new: String,
620}
621
622impl Replaced {
623 pub fn between(before: &str, after: &str) -> Replaced {
624 let (start, old_end, new_end) = diff(before, after);
625 Replaced {
626 range: start..old_end,
627 old: before[start..old_end].to_string(),
628 new: after[start..new_end].to_string(),
629 }
630 }
631
632 pub fn new_range(&self) -> Range<usize> {
633 self.range.start..self.range.start + self.new.len()
634 }
635
636 pub fn is_empty(&self) -> bool {
637 self.old == self.new
638 }
639
640 pub fn rebase(&self, base: &str, pending: &str) -> Option<String> {
641 if base.get(self.range.clone()) != Some(self.old.as_str()) {
642 return None;
643 }
644 let splice = |text: &str, at: Range<usize>| {
645 let mut out = String::with_capacity(text.len() + self.new.len());
646 out.push_str(&text[..at.start]);
647 out.push_str(&self.new);
648 out.push_str(&text[at.end..]);
649 out
650 };
651 if base == pending {
652 return Some(splice(base, self.range.clone()));
653 }
654 let (start, base_end, pending_end) = diff(base, pending);
655 if base_end < self.range.start {
656 let shift = pending_end as isize - base_end as isize;
657 let at = (self.range.start as isize + shift) as usize
658 ..(self.range.end as isize + shift) as usize;
659 Some(splice(pending, at))
660 } else if start > self.range.end {
661 Some(splice(pending, self.range.clone()))
662 } else {
663 None
664 }
665 }
666}
667
668#[derive(Clone, Debug)]
669struct Exact {
670 range: Range<usize>,
671 old: String,
672 new: String,
673 stamp: Stamp,
674 back: Vec<Op>,
675}
676
677#[derive(Clone, Debug)]
678struct Applied {
679 inverse: Vec<Op>,
680 replaced: Replaced,
681 after: Stamp,
682}
683
684#[derive(Clone, Debug)]
685pub struct TomlPatch {
686 label: String,
687 file: PathBuf,
688 ops: Vec<Op>,
689 exact: Option<Exact>,
690 applied: Option<Applied>,
691}
692
693impl TomlPatch {
694 pub fn new(file: impl Into<PathBuf>, ops: Vec<Op>) -> TomlPatch {
695 let label = match ops.first() {
696 Some(op) => format!("{} {}", op.verb(), path_text(op.path())),
697 None => "nothing".to_string(),
698 };
699 TomlPatch {
700 label,
701 file: file.into(),
702 ops,
703 exact: None,
704 applied: None,
705 }
706 }
707
708 pub fn set(file: impl Into<PathBuf>, path: Vec<Key>, value: impl Into<Item>) -> TomlPatch {
709 TomlPatch::new(
710 file,
711 vec![Op::Set {
712 path,
713 value: value.into(),
714 }],
715 )
716 }
717
718 pub fn unset(file: impl Into<PathBuf>, path: Vec<Key>) -> TomlPatch {
719 TomlPatch::new(file, vec![Op::Unset { path }])
720 }
721
722 pub fn remove(file: impl Into<PathBuf>, path: Vec<Key>) -> TomlPatch {
723 TomlPatch::unset(file, path).labelled_as("remove")
724 }
725
726 pub fn push(file: impl Into<PathBuf>, path: Vec<Key>, value: impl Into<Item>) -> TomlPatch {
727 TomlPatch::new(
728 file,
729 vec![Op::Push {
730 path,
731 value: value.into(),
732 }],
733 )
734 }
735
736 pub fn insert(file: impl Into<PathBuf>, path: Vec<Key>, value: impl Into<Item>) -> TomlPatch {
737 TomlPatch::new(
738 file,
739 vec![Op::Insert {
740 path,
741 value: value.into(),
742 }],
743 )
744 }
745
746 fn labelled_as(mut self, verb: &str) -> TomlPatch {
747 if let Some(op) = self.ops.first() {
748 self.label = format!("{verb} {}", path_text(op.path()));
749 }
750 self
751 }
752
753 pub fn labelled(mut self, label: impl Into<String>) -> TomlPatch {
754 self.label = label.into();
755 self
756 }
757
758 pub fn and(mut self, op: Op) -> TomlPatch {
759 self.ops.push(op);
760 self
761 }
762
763 pub fn file(&self) -> &Path {
764 &self.file
765 }
766
767 pub fn ops(&self) -> &[Op] {
768 &self.ops
769 }
770
771 pub fn replaced(&self) -> Option<&Replaced> {
772 self.applied.as_ref().map(|applied| &applied.replaced)
773 }
774
775 fn exact_after(&self, before: &str) -> Option<(String, Vec<Op>)> {
776 let exact = self.exact.as_ref()?;
777 if Stamp::of(before) != exact.stamp || before.get(exact.range.clone())? != exact.old {
778 return None;
779 }
780 let mut after = String::with_capacity(before.len() + exact.new.len());
781 after.push_str(&before[..exact.range.start]);
782 after.push_str(&exact.new);
783 after.push_str(&before[exact.range.end..]);
784 Some((after, exact.back.clone()))
785 }
786}
787
788impl Edit for TomlPatch {
789 fn label(&self) -> &str {
790 &self.label
791 }
792
793 fn files(&self) -> Vec<PathBuf> {
794 vec![self.file.clone()]
795 }
796
797 fn apply(&mut self, files: &mut Files) -> Result<(), DocError> {
798 let file = files
799 .get(&self.file)
800 .ok_or_else(|| DocError::new(&self.file, "", "the file is not open"))?;
801 let doc = file.doc().ok_or_else(|| {
802 DocError::new(
803 &self.file,
804 "",
805 "the file does not parse, so no patch applies until it does",
806 )
807 })?;
808 let before = file.text();
809 let (after, inverse) =
810 match self.exact_after(before) {
811 Some(exact) => exact,
812 None => {
813 let mut doc = doc.clone();
814 let mut inverse = Vec::with_capacity(self.ops.len());
815 for op in &self.ops {
816 inverse.push(edit(&mut doc, op).map_err(|text| {
817 DocError::new(&self.file, path_text(op.path()), text)
818 })?);
819 }
820 inverse.reverse();
821 (doc.to_string(), inverse)
822 }
823 };
824 let parsed = after.parse::<DocumentMut>().map_err(|error| {
825 DocError::new(
826 &self.file,
827 "",
828 format!(
829 "the patch would leave text that does not parse: {}",
830 error.message()
831 ),
832 )
833 })?;
834 let replaced = Replaced::between(before, &after);
835 let stamp = Stamp::of(&after);
836 files.set_parsed(&self.file, after, parsed)?;
837 self.applied = Some(Applied {
838 inverse,
839 replaced,
840 after: stamp,
841 });
842 Ok(())
843 }
844
845 fn inverse(&self) -> Box<dyn Edit> {
846 let Some(applied) = &self.applied else {
847 return Box::new(TomlPatch::new(self.file.clone(), Vec::new()).labelled(&self.label));
848 };
849 let replaced = &applied.replaced;
850 Box::new(TomlPatch {
851 label: self.label.clone(),
852 file: self.file.clone(),
853 ops: applied.inverse.clone(),
854 exact: Some(Exact {
855 range: replaced.new_range(),
856 old: replaced.new.clone(),
857 new: replaced.old.clone(),
858 stamp: applied.after,
859 back: self.ops.clone(),
860 }),
861 applied: None,
862 })
863 }
864}
865
866#[cfg(test)]
867mod tests {
868 use super::*;
869 use toml_edit::value;
870
871 fn path(text: &str) -> Vec<Key> {
872 parse_path(text).unwrap()
873 }
874
875 fn doc(text: &str) -> DocumentMut {
876 text.parse().unwrap()
877 }
878
879 #[test]
880 fn a_path_reads_and_writes_as_text() {
881 let keys = path("object[crate].at[0]");
882 assert_eq!(
883 keys,
884 vec![
885 Key::Table("object".into()),
886 Key::Object("crate".into()),
887 Key::Table("at".into()),
888 Key::Index(0),
889 ]
890 );
891 assert_eq!(path_text(&keys), "object[crate].at[0]");
892 let quoted = path("\"a.b\".c");
893 assert_eq!(quoted[0], Key::Table("a.b".into()));
894 assert_eq!(path_text("ed), "\"a.b\".c");
895 for bad in ["", "a.", "a[", "a[]", "a b", "a..b", ".a"] {
896 assert!(parse_path(bad).is_err(), "{bad}");
897 }
898 }
899
900 #[test]
901 fn set_keeps_the_values_comment_and_its_inverse_restores_it() {
902 let text = "[box]\nat = [0.0, 0.05, 0.0] # sits on the floor\n";
903 let mut d = doc(text);
904 let back = edit(
905 &mut d,
906 &Op::Set {
907 path: path("box.at[1]"),
908 value: value(0.5),
909 },
910 )
911 .unwrap();
912 assert_eq!(
913 d.to_string(),
914 "[box]\nat = [0.0, 0.5, 0.0] # sits on the floor\n"
915 );
916 edit(&mut d, &back).unwrap();
917 assert_eq!(d.to_string(), text);
918 }
919
920 #[test]
921 fn entries_are_found_by_name_and_renames_invert() {
922 let text = "[[object]]\nname = \"a\"\n\n[[object]]\nname = \"b\"\nsize = 1\n";
923 let mut d = doc(text);
924 let back = edit(
925 &mut d,
926 &Op::Set {
927 path: path("object[b].name"),
928 value: value("c"),
929 },
930 )
931 .unwrap();
932 assert_eq!(back.path(), path("object[c].name").as_slice());
933 edit(&mut d, &back).unwrap();
934 assert_eq!(d.to_string(), text);
935 }
936
937 #[test]
938 fn push_and_remove_invert_each_other() {
939 let text = "list = [1, 2]\n\n[[object]]\nname = \"a\"\n";
940 let mut d = doc(text);
941 let back = edit(
942 &mut d,
943 &Op::Push {
944 path: path("list"),
945 value: value(3),
946 },
947 )
948 .unwrap();
949 assert_eq!(d["list"].as_array().unwrap().len(), 3);
950 assert_eq!(back.path(), path("list[2]").as_slice());
951 edit(&mut d, &back).unwrap();
952 assert_eq!(d.to_string(), text);
953 let back = edit(
954 &mut d,
955 &Op::Unset {
956 path: path("list[0]"),
957 },
958 )
959 .unwrap();
960 assert!(matches!(back, Op::Insert { .. }));
961 edit(&mut d, &back).unwrap();
962 assert_eq!(d.to_string(), text);
963 let back = edit(
964 &mut d,
965 &Op::Unset {
966 path: path("object[a]"),
967 },
968 )
969 .unwrap();
970 assert!(
971 d.get("object")
972 .unwrap()
973 .as_array_of_tables()
974 .unwrap()
975 .is_empty()
976 );
977 edit(&mut d, &back).unwrap();
978 assert_eq!(d.to_string(), text);
979 }
980
981 #[test]
982 fn inserted_elements_follow_their_neighbours_spacing() {
983 let mut d = doc("a = [\"x\", \"y\"]\nb = [\n 1,\n 2,\n]\n");
984 for op in [
985 Op::Insert {
986 path: path("a[0]"),
987 value: value("w"),
988 },
989 Op::Push {
990 path: path("a"),
991 value: value("z"),
992 },
993 Op::Push {
994 path: path("b"),
995 value: value(3),
996 },
997 Op::Insert {
998 path: path("b[0]"),
999 value: value(0),
1000 },
1001 ] {
1002 edit(&mut d, &op).unwrap();
1003 }
1004 assert_eq!(
1005 d.to_string(),
1006 "a = [\"w\", \"x\", \"y\", \"z\"]\nb = [\n 0,\n 1,\n 2,\n 3,\n]\n"
1007 );
1008 }
1009
1010 #[test]
1011 fn push_onto_a_missing_key_makes_the_array() {
1012 let mut d = doc("a = 1\n");
1013 let back = edit(
1014 &mut d,
1015 &Op::Push {
1016 path: path("tags"),
1017 value: value("x"),
1018 },
1019 )
1020 .unwrap();
1021 assert_eq!(d["tags"].as_array().unwrap().len(), 1);
1022 edit(&mut d, &back).unwrap();
1023 assert_eq!(d.to_string(), "a = 1\n");
1024 }
1025
1026 #[test]
1027 fn bad_paths_are_refused_with_a_reason() {
1028 let mut d = doc("a = 1\nlist = [1]\n[t]\nx = { y = 1 }\n");
1029 let cases = [
1030 Op::Unset { path: path("b") },
1031 Op::Set {
1032 path: path("a.b"),
1033 value: value(1),
1034 },
1035 Op::Set {
1036 path: path("list[5]"),
1037 value: value(1),
1038 },
1039 Op::Insert {
1040 path: path("t.x"),
1041 value: value(1),
1042 },
1043 Op::Set {
1044 path: path("list.x"),
1045 value: value(1),
1046 },
1047 ];
1048 for op in cases {
1049 assert!(edit(&mut d, &op).is_err(), "{op:?}");
1050 }
1051 assert_eq!(d.to_string(), "a = 1\nlist = [1]\n[t]\nx = { y = 1 }\n");
1052 }
1053
1054 #[test]
1055 fn a_table_value_set_into_an_inline_table_becomes_inline() {
1056 let mut d = doc("x = { y = 1 }\n");
1057 let mut table = Table::new();
1058 table.insert("z", value(2));
1059 edit(
1060 &mut d,
1061 &Op::Set {
1062 path: path("x.w"),
1063 value: Item::Table(table),
1064 },
1065 )
1066 .unwrap();
1067 assert_eq!(d["x"]["w"]["z"].as_integer(), Some(2));
1068 assert!(d.to_string().parse::<DocumentMut>().is_ok());
1069 }
1070
1071 #[test]
1072 fn value_at_reads_a_value_or_a_tables_values() {
1073 let d = doc("[[object]]\nname = \"a\"\nat = [1, 2]\n");
1074 assert_eq!(
1075 value_at(&d, &path("object[a].at[1]")).unwrap().as_integer(),
1076 Some(2)
1077 );
1078 assert!(value_at(&d, &path("object[a]")).unwrap().is_inline_table());
1079 assert!(value_at(&d, &path("object[b]")).is_none());
1080 assert!(exists(&d, &path("object[0].at")));
1081 }
1082
1083 #[test]
1084 fn a_replaced_range_rebases_pending_text_or_refuses_it() {
1085 let base = "a = 1\nb = 2\nc = 3\n";
1086 let replaced = Replaced::between(base, "a = 1\nb = 20\nc = 3\n");
1087 assert_eq!(replaced.range, 11..11);
1088 assert_eq!(
1089 replaced.rebase(base, "a = 100\nb = 2\nc = 3\n").as_deref(),
1090 Some("a = 100\nb = 20\nc = 3\n")
1091 );
1092 assert_eq!(
1093 replaced.rebase(base, "a = 1\nb = 2\nc = 30\n").as_deref(),
1094 Some("a = 1\nb = 20\nc = 30\n")
1095 );
1096 assert_eq!(replaced.rebase(base, "a = 1\nb = 25\nc = 3\n"), None);
1097 assert_eq!(
1098 replaced.rebase(base, base).as_deref(),
1099 Some("a = 1\nb = 20\nc = 3\n")
1100 );
1101 }
1102}