1use crate::document::{DocumentError, DocumentResult, Value};
4
5pub fn set_preserving(content: &str, path: &str, value: &Value) -> DocumentResult<String> {
14 keep_bom(content, |body| set_body(body, path, value))
15}
16
17fn set_body(content: &str, path: &str, value: &Value) -> DocumentResult<String> {
21 let segments = crate::document::parse_path(path)?;
22 let mut document =
23 content
24 .parse::<toml_edit::DocumentMut>()
25 .map_err(|error| DocumentError::ParseError {
26 format: "TOML".to_string(),
27 detail: error.to_string(),
28 })?;
29 let (last, parents) = segments.split_last().ok_or(DocumentError::EmptyPath)?;
30 let mut current = document.as_item_mut();
31 for parent in parents {
32 if current.is_array_of_tables() {
33 return Err(collection_refusal(
34 "editing an array of tables requires an explicit element identity",
35 ));
36 }
37 if current
38 .as_value()
39 .and_then(toml_edit::Value::as_array)
40 .is_some()
41 {
42 let index = parent
43 .parse::<usize>()
44 .map_err(|_| DocumentError::UnregisteredArray {
45 path: path.to_string(),
46 })?;
47 current = current
48 .get_mut(index)
49 .ok_or_else(|| DocumentError::PathNotFound {
50 path: path.to_string(),
51 })?;
52 continue;
53 }
54 {
59 let table =
60 current
61 .as_table_like_mut()
62 .ok_or_else(|| DocumentError::UnsupportedOperation {
63 format: "TOML".to_string(),
64 operation: "set".to_string(),
65 detail: "cannot address a key inside a non-table TOML value".to_string(),
66 })?;
67 if table.get(parent).filter(|item| !item.is_none()).is_none() {
68 let mut created = toml_edit::Table::new();
69 created.set_implicit(true);
70 table.insert(parent, toml_edit::Item::Table(created));
71 hand_closing_space_to_new_entry(current);
72 }
73 }
74 current = current
75 .get_mut(parent)
76 .filter(|item| !item.is_none())
77 .ok_or_else(|| DocumentError::PathNotFound {
78 path: path.to_string(),
79 })?;
80 }
81 if current
82 .as_value()
83 .and_then(toml_edit::Value::as_array)
84 .is_some()
85 {
86 if last == "-" {
87 let parent_key = crate::document::join_path(parents);
88 let mut values = match crate::document::get_path(
89 &load(content)?,
90 &parent_key,
91 crate::document::Addressing::INDEX_ONLY,
92 )? {
93 Value::Array(values) => values,
94 _ => return Err(collection_refusal("append target is not an array")),
95 };
96 let unchanged = values.len();
97 values.push(value.clone());
98 let existing = current
99 .as_array()
100 .ok_or_else(|| collection_refusal("append target is not an array"))?;
101 *current = toml_edit::Item::Value(array_value_with_prefix(
102 &values,
103 Some(existing),
104 unchanged,
105 )?);
106 return Ok(preserve_line_endings(content, document.to_string()));
107 }
108 let index = last
109 .parse::<usize>()
110 .map_err(|_| DocumentError::UnregisteredArray {
111 path: path.to_string(),
112 })?;
113 let target = current
114 .get_mut(index)
115 .ok_or_else(|| DocumentError::PathNotFound {
116 path: path.to_string(),
117 })?;
118 replace_item_preserving(target, value)?;
119 } else if current.is_array_of_tables() {
120 return Err(collection_refusal(
123 "editing an array of tables requires an explicit element identity",
124 ));
125 } else {
126 let table =
127 current
128 .as_table_like_mut()
129 .ok_or_else(|| DocumentError::UnsupportedOperation {
130 format: "TOML".to_string(),
131 operation: "set".to_string(),
132 detail: "cannot address a key inside a non-table TOML value".to_string(),
133 })?;
134 match table.get_mut(last).filter(|item| !item.is_none()) {
135 Some(target) => replace_item_preserving(target, value)?,
136 None => {
138 table.insert(last, toml_item(value)?);
139 hand_closing_space_to_new_entry(current);
140 }
141 }
142 }
143 Ok(preserve_line_endings(content, document.to_string()))
144}
145
146fn hand_closing_space_to_new_entry(item: &mut toml_edit::Item) {
153 let Some(inline) = item.as_inline_table_mut() else {
154 return;
155 };
156 let count = inline.len();
157 if count < 2 {
158 return;
159 }
160 let moved = inline.iter_mut().nth(count - 2).and_then(|(_, previous)| {
161 let suffix = previous.decor().suffix()?.as_str()?.to_string();
162 (!suffix.is_empty() && suffix.trim().is_empty()).then(|| {
163 previous.decor_mut().set_suffix("");
164 suffix
165 })
166 });
167 if let Some((_, added)) = inline.iter_mut().last() {
168 added.decor_mut().set_suffix(moved.unwrap_or_default());
169 }
170}
171
172pub fn unset_preserving(content: &str, path: &str) -> DocumentResult<String> {
174 keep_bom(content, |body| unset_body(body, path))
175}
176
177fn unset_body(content: &str, path: &str) -> DocumentResult<String> {
178 let segments = crate::document::parse_path(path)?;
179 let (last, parents) = segments.split_last().ok_or(DocumentError::EmptyPath)?;
180 let mut document =
181 content
182 .parse::<toml_edit::DocumentMut>()
183 .map_err(|error| DocumentError::ParseError {
184 format: "TOML".to_string(),
185 detail: error.to_string(),
186 })?;
187 let mut current = document.as_item_mut();
190 for parent in parents {
191 if current.is_array_of_tables() {
192 return Err(DocumentError::UnsupportedOperation {
193 format: "TOML".to_string(),
194 operation: "unset".to_string(),
195 detail: "editing an array of tables requires an explicit element identity"
196 .to_string(),
197 });
198 }
199 let next = if current.as_array().is_some() {
200 parent
201 .parse::<usize>()
202 .ok()
203 .and_then(|index| current.get_mut(index))
204 } else {
205 current.get_mut(parent)
206 };
207 current =
208 next.filter(|item| !item.is_none())
209 .ok_or_else(|| DocumentError::PathNotFound {
210 path: path.to_string(),
211 })?;
212 }
213 if current.is_array_of_tables() {
214 return Err(DocumentError::UnsupportedOperation {
215 format: "TOML".to_string(),
216 operation: "unset".to_string(),
217 detail: "editing an array of tables requires an explicit element identity".to_string(),
218 });
219 }
220 if let Some(array) = current.as_array_mut() {
221 let index = last
222 .parse::<usize>()
223 .ok()
224 .filter(|index| *index < array.len())
225 .ok_or_else(|| DocumentError::PathNotFound {
226 path: path.to_string(),
227 })?;
228 let removed = array.remove(index);
229 remove_element_comment(array, index, &removed);
230 return Ok(preserve_line_endings(content, document.to_string()));
231 }
232 let table = current
237 .as_table_like_mut()
238 .ok_or_else(|| DocumentError::UnsupportedOperation {
239 format: "TOML".to_string(),
240 operation: "unset".to_string(),
241 detail: "cannot address a key inside a non-table TOML value".to_string(),
242 })?;
243 let Some(removed) = table.remove(last) else {
244 return Err(DocumentError::PathNotFound {
245 path: path.to_string(),
246 });
247 };
248 let removed_suffix = removed
253 .as_value()
254 .and_then(|value| value.decor().suffix())
255 .cloned();
256 if let (Some(inline), Some(suffix)) = (current.as_inline_table_mut(), removed_suffix)
257 && let Some((_, final_value)) = inline.iter_mut().last()
258 {
259 let carries_its_own = final_value
260 .decor()
261 .suffix()
262 .and_then(|raw| raw.as_str())
263 .is_some_and(|text| !text.is_empty());
264 if !carries_its_own {
265 final_value.decor_mut().set_suffix(suffix);
266 }
267 }
268 Ok(preserve_line_endings(content, document.to_string()))
269}
270
271fn keep_bom(
274 content: &str,
275 edit: impl FnOnce(&str) -> DocumentResult<String>,
276) -> DocumentResult<String> {
277 match content.strip_prefix('\u{feff}') {
278 Some(body) => edit(body).map(|edited| format!("\u{feff}{edited}")),
279 None => edit(content),
280 }
281}
282
283fn preserve_line_endings(content: &str, rendered: String) -> String {
284 if content.contains("\r\n")
285 && content.as_bytes().iter().enumerate().all(|(index, byte)| {
286 *byte != b'\n' || (index > 0 && content.as_bytes()[index - 1] == b'\r')
287 })
288 {
289 rendered.replace("\r\n", "\n").replace('\n', "\r\n")
290 } else {
291 rendered
292 }
293}
294
295fn toml_item(value: &Value) -> DocumentResult<toml_edit::Item> {
296 toml_value(value, None).map(toml_edit::Item::Value)
297}
298
299fn toml_value(
300 value: &Value,
301 existing: Option<&toml_edit::Value>,
302) -> DocumentResult<toml_edit::Value> {
303 let mut converted = match value {
304 Value::Null => Err(DocumentError::UnsupportedOperation {
305 format: "TOML".to_string(),
306 operation: "set".to_string(),
307 detail: "TOML has no null value".to_string(),
308 }),
309 Value::Bool(value) => Ok(toml_edit::Value::from(*value)),
310 Value::Integer(value) => Ok(toml_edit::Value::from(*value)),
311 Value::Unsigned(value) => i64::try_from(*value)
312 .map(toml_edit::Value::from)
313 .map_err(|_| DocumentError::UnsupportedOperation {
314 format: "TOML".to_string(),
315 operation: "set".to_string(),
316 detail: "unsigned integer exceeds TOML i64 range".to_string(),
317 }),
318 Value::Float(value) if value.is_finite() => Ok(toml_edit::Value::from(*value)),
319 Value::Float(_) => Err(DocumentError::UnsupportedOperation {
320 format: "TOML".to_string(),
321 operation: "set".to_string(),
322 detail: "non-finite TOML float is not representable".to_string(),
323 }),
324 Value::Number(text) if value.is_float() => text
333 .parse::<f64>()
334 .ok()
335 .filter(|value| value.is_finite())
336 .map(toml_edit::Value::from)
337 .ok_or_else(|| DocumentError::UnsupportedOperation {
338 format: "TOML".to_string(),
339 operation: "set".to_string(),
340 detail: "float literal is not representable in TOML".to_string(),
341 }),
342 Value::Number(_) => Err(DocumentError::UnsupportedOperation {
343 format: "TOML".to_string(),
344 operation: "set".to_string(),
345 detail: "integer literal exceeds TOML's 64-bit integer range".to_string(),
346 }),
347 Value::String(value) => {
348 let existing_datetime = existing.filter(|item| item.is_datetime());
352 if let Some(existing) = existing_datetime.filter(|item| {
353 item.as_datetime()
354 .is_some_and(|datetime| datetime.to_string() == *value)
355 }) {
356 Ok(existing.clone())
357 } else if let Some(datetime) =
358 existing_datetime.and_then(|_| value.parse::<toml_edit::Datetime>().ok())
359 {
360 Ok(toml_edit::Value::from(datetime))
361 } else if value.contains('\n') {
362 single_line_string(value)
363 } else {
364 Ok(toml_edit::Value::from(value.clone()))
365 }
366 }
367 Value::Array(values) => array_value(values, existing.and_then(toml_edit::Value::as_array)),
368 Value::Object(values) => {
369 inline_table_value(values, existing.and_then(toml_edit::Value::as_inline_table))
370 }
371 }?;
372 if let Some(existing) = existing {
373 *converted.decor_mut() = existing.decor().clone();
374 }
375 Ok(converted)
376}
377
378fn single_line_string(value: &str) -> DocumentResult<toml_edit::Value> {
387 use std::fmt::Write as _;
388 let mut literal = String::with_capacity(value.len() + 2);
389 literal.push('"');
390 for character in value.chars() {
391 match character {
392 '\\' => literal.push_str("\\\\"),
393 '"' => literal.push_str("\\\""),
394 '\n' => literal.push_str("\\n"),
395 '\r' => literal.push_str("\\r"),
396 '\t' => literal.push_str("\\t"),
397 control if control.is_control() => {
398 let _ = write!(literal, "\\u{:04X}", u32::from(control));
399 }
400 other => literal.push(other),
401 }
402 }
403 literal.push('"');
404 let mut parsed = literal.parse::<toml_edit::Value>().map_err(|error| {
405 DocumentError::UnsupportedOperation {
406 format: "TOML".to_string(),
407 operation: "set".to_string(),
408 detail: format!("string is not representable in TOML: {error}"),
409 }
410 })?;
411 parsed.decor_mut().clear();
414 Ok(parsed)
415}
416
417fn layout_without_comments(decor: &str) -> String {
426 let mut kept = String::with_capacity(decor.len());
427 let mut rest = decor;
428 while let Some(hash) = rest.find('#') {
429 kept.push_str(rest[..hash].trim_end_matches([' ', '\t']));
432 match rest[hash..].find('\n') {
433 Some(newline) => rest = &rest[hash + newline..],
435 None => return kept,
437 }
438 }
439 kept.push_str(rest);
440 kept
441}
442
443fn remove_element_comment(array: &mut toml_edit::Array, index: usize, removed: &toml_edit::Value) {
458 let decor_text = |raw: Option<&toml_edit::RawString>| {
459 raw.and_then(toml_edit::RawString::as_str)
460 .unwrap_or_default()
461 .to_string()
462 };
463 let removed_prefix = decor_text(removed.decor().prefix());
464 if index == 0
465 && let Some(first) = array.get_mut(0)
466 {
467 let prefix = decor_text(first.decor().prefix());
468 let beside_bracket = removed_prefix.split('\n').next().unwrap_or_default();
469 let layout = if prefix.contains('\n') {
470 format!("{beside_bracket}{}", after_first_line(&prefix))
474 } else if removed_prefix.contains('\n') {
475 let indentation = &removed_prefix[removed_prefix.rfind('\n').unwrap_or_default()..];
478 format!("{beside_bracket}{indentation}")
479 } else {
480 removed_prefix
483 };
484 first.decor_mut().set_prefix(layout);
485 return;
486 }
487 let carried = comment_line(&removed_prefix);
490 if let Some(next) = array.get_mut(index) {
491 let prefix = decor_text(next.decor().prefix());
492 let rest = after_first_line(&prefix);
493 match (carried, comment_line(&prefix)) {
494 (Some(comment), _) => next.decor_mut().set_prefix(format!("{comment}{rest}")),
495 (None, Some(_)) => next.decor_mut().set_prefix(rest.to_string()),
496 (None, None) => {}
497 }
498 return;
499 }
500 let tail = format!(
501 "{}{}",
502 decor_text(removed.decor().suffix()),
503 array.trailing().as_str().unwrap_or_default()
504 );
505 let rest = after_first_line(&tail);
506 let trailing = match (carried, comment_line(&tail)) {
507 (Some(comment), _) if rest.is_empty() => format!("{comment}\n"),
509 (Some(comment), _) => format!("{comment}{rest}"),
510 (None, Some(_)) => rest.to_string(),
511 (None, None) => tail.clone(),
512 };
513 array.set_trailing(trailing);
514}
515
516fn comment_line(decor: &str) -> Option<&str> {
519 let line = decor.split('\n').next().unwrap_or_default();
520 line.contains('#').then_some(line)
521}
522
523fn after_first_line(decor: &str) -> &str {
525 decor.find('\n').map_or("", |newline| &decor[newline..])
526}
527
528fn first_comment(decor: &str) -> Option<&str> {
530 let start = decor.find('#')?;
531 let end = decor[start..]
532 .find('\n')
533 .map_or(decor.len(), |newline| start + newline);
534 Some(&decor[start..end])
535}
536
537fn with_first_comment(decor: &str, replacement: Option<&str>) -> String {
539 let Some(start) = decor.find('#') else {
540 return decor.to_string();
541 };
542 let end = decor[start..]
543 .find('\n')
544 .map_or(decor.len(), |newline| start + newline);
545 let mut out = String::with_capacity(decor.len());
546 match replacement {
547 Some(comment) => {
548 out.push_str(&decor[..start]);
549 out.push_str(comment);
550 }
551 None => out.push_str(decor[..start].trim_end_matches([' ', '\t'])),
552 }
553 out.push_str(&decor[end..]);
554 out
555}
556
557fn array_value(
558 values: &[Value],
559 existing: Option<&toml_edit::Array>,
560) -> DocumentResult<toml_edit::Value> {
561 array_value_with_prefix(values, existing, 0)
562}
563
564fn array_value_with_prefix(
566 values: &[Value],
567 existing: Option<&toml_edit::Array>,
568 unchanged: usize,
569) -> DocumentResult<toml_edit::Value> {
570 let existing_values = existing
571 .map(|array| array.iter().cloned().collect::<Vec<_>>())
572 .unwrap_or_default();
573 let mut array = existing.cloned().unwrap_or_default();
574 if array.len() > values.len() {
575 let surviving_comment = existing_values
582 .get(values.len())
583 .and_then(|item| item.decor().prefix())
584 .and_then(toml_edit::RawString::as_str)
585 .and_then(first_comment)
586 .map(str::to_string);
587 while array.len() > values.len() {
588 let last = array.len() - 1;
589 array.remove(last);
590 }
591 let trailing = array.trailing().as_str().unwrap_or_default().to_string();
592 if first_comment(&trailing).is_some() {
593 array.set_trailing(with_first_comment(&trailing, surviving_comment.as_deref()));
594 }
595 }
596 let length_before_append = array.len();
597 if values.len() > length_before_append
603 && let Some(last) = length_before_append
604 .checked_sub(1)
605 .and_then(|index| array.get_mut(index))
606 {
607 let suffix = last
608 .decor()
609 .suffix()
610 .and_then(toml_edit::RawString::as_str)
611 .unwrap_or_default()
612 .to_string();
613 if suffix.contains(['#', '\n']) {
614 last.decor_mut().set_suffix("");
615 let trailing = array.trailing().as_str().unwrap_or_default().to_string();
616 array.set_trailing(format!("{suffix}{trailing}"));
617 }
618 }
619 for (index, value) in values.iter().enumerate() {
620 if index < unchanged {
621 continue;
622 }
623 let hint = existing_values.get(index);
624 let mut converted = toml_value(value, hint)?;
625 if index < array.len() {
626 array.replace_formatted(index, converted);
627 } else {
628 let layout = existing_values
629 .last()
630 .and_then(|item| item.decor().prefix())
631 .and_then(toml_edit::RawString::as_str)
632 .map(layout_without_comments)
633 .unwrap_or_default();
634 let layout = if layout.is_empty() && index > 0 {
637 " ".to_string()
638 } else {
639 layout
640 };
641 converted.decor_mut().set_prefix(layout);
642 if index == length_before_append {
643 let trailing = array.trailing().as_str().unwrap_or_default().to_string();
646 if first_comment(&trailing).is_some() {
647 let layout = converted
648 .decor()
649 .prefix()
650 .and_then(toml_edit::RawString::as_str)
651 .unwrap_or_default();
652 let indent = layout.rsplit('\n').next().unwrap_or_default().to_string();
653 converted.decor_mut().set_prefix(format!(
654 "{}{indent}",
655 trailing.trim_end_matches([' ', '\t'])
656 ));
657 let bracket_indent = trailing.rsplit('\n').next().unwrap_or_default();
660 array.set_trailing(format!("\n{bracket_indent}"));
661 }
662 }
663 array.push_formatted(converted);
664 }
665 }
666 if values.len() > length_before_append
672 && let Some(last_index) = length_before_append.checked_sub(1)
673 {
674 let suffix = array
675 .get(last_index)
676 .and_then(|item| item.decor().suffix())
677 .and_then(toml_edit::RawString::as_str)
678 .filter(|suffix| !suffix.is_empty() && suffix.chars().all(|c| c == ' ' || c == '\t'))
679 .map(str::to_string);
680 let trailing_is_empty = array.trailing().as_str().is_none_or(str::is_empty);
681 if let Some(suffix) = suffix
682 && trailing_is_empty
683 {
684 if let Some(item) = array.get_mut(last_index) {
685 item.decor_mut().set_suffix("");
686 }
687 array.set_trailing(suffix);
688 }
689 }
690 if values.is_empty() {
691 array.set_trailing_comma(false);
692 }
693 if existing.is_none() {
694 array.fmt();
695 }
696 Ok(toml_edit::Value::Array(array))
697}
698
699fn inline_table_value(
700 values: &std::collections::BTreeMap<String, Value>,
701 existing: Option<&toml_edit::InlineTable>,
702) -> DocumentResult<toml_edit::Value> {
703 let mut table = existing.cloned().unwrap_or_default();
704 table.retain(|key, _| values.contains_key(key));
705 for (key, value) in values {
706 if let Some(current) = table.get_mut(key) {
707 *current = toml_value(value, Some(current))?;
708 } else {
709 table.insert(key, toml_value(value, None)?);
710 }
711 }
712 if values.is_empty() {
713 table.set_trailing_comma(false);
714 }
715 if existing.is_none() {
716 table.fmt();
717 }
718 Ok(toml_edit::Value::InlineTable(table))
719}
720
721fn replace_item_preserving(target: &mut toml_edit::Item, value: &Value) -> DocumentResult<()> {
722 if target.is_array_of_tables() {
723 return Err(DocumentError::UnsupportedOperation {
724 format: "TOML".to_string(),
725 operation: "set".to_string(),
726 detail: "editing an array of tables requires an explicit element identity".to_string(),
727 });
728 }
729 if let (Some(table), Value::Object(values)) = (target.as_table_mut(), value) {
730 return sync_table(table, values);
731 }
732 let converted = toml_value(value, target.as_value())?;
733 *target = toml_edit::Item::Value(converted);
734 Ok(())
735}
736
737fn sync_table(
738 table: &mut toml_edit::Table,
739 values: &std::collections::BTreeMap<String, Value>,
740) -> DocumentResult<()> {
741 table.retain(|key, _| values.contains_key(key));
742 for (key, value) in values {
743 if let Some(current) = table.get_mut(key) {
744 replace_item_preserving(current, value)?;
745 } else {
746 table.insert(key, toml_item(value)?);
747 }
748 }
749 Ok(())
750}
751
752fn collection_refusal(detail: &str) -> DocumentError {
753 DocumentError::UnsupportedOperation {
754 format: "TOML".to_string(),
755 operation: "set".to_string(),
756 detail: detail.to_string(),
757 }
758}
759
760pub fn load(content: &str) -> DocumentResult<Value> {
761 toml::from_str::<toml::Value>(content)
762 .map(value_to_our_value)
763 .map_err(|e| DocumentError::ParseError {
764 format: "TOML".to_string(),
765 detail: e.to_string(),
766 })
767}
768
769pub fn save(value: &Value) -> DocumentResult<String> {
770 let toml_val = our_value_to_toml_value(value)?;
771 toml::to_string_pretty(&toml_val).map_err(|e| DocumentError::ParseError {
772 format: "TOML".to_string(),
773 detail: e.to_string(),
774 })
775}
776
777fn value_to_our_value(v: toml::Value) -> Value {
778 match v {
779 toml::Value::Boolean(b) => Value::Bool(b),
780 toml::Value::Integer(i) => Value::Integer(i),
781 toml::Value::Float(f) => Value::Float(f),
782 toml::Value::String(s) => Value::String(s),
783 toml::Value::Array(a) => Value::Array(a.into_iter().map(value_to_our_value).collect()),
784 toml::Value::Table(t) => {
785 let map = t
786 .into_iter()
787 .map(|(k, v)| (k, value_to_our_value(v)))
788 .collect();
789 Value::Object(map)
790 }
791 toml::Value::Datetime(dt) => Value::String(dt.to_string()),
792 }
793}
794
795fn our_value_to_toml_value(v: &Value) -> DocumentResult<toml::Value> {
796 match v {
797 Value::Null => Err(DocumentError::UnsupportedOperation {
798 format: "TOML".to_string(),
799 operation: "save".to_string(),
800 detail: "TOML has no null value".to_string(),
801 }),
802 Value::Bool(b) => Ok(toml::Value::Boolean(*b)),
803 Value::Integer(i) => Ok(toml::Value::Integer(*i)),
804 Value::Unsigned(i) => i64::try_from(*i).map(toml::Value::Integer).map_err(|_| {
805 DocumentError::UnsupportedOperation {
806 format: "TOML".to_string(),
807 operation: "save".to_string(),
808 detail: "unsigned integer exceeds TOML i64 range".to_string(),
809 }
810 }),
811 Value::Float(f) => Ok(toml::Value::Float(*f)),
812 Value::Number(text) if v.is_float() => text
813 .parse::<f64>()
814 .ok()
815 .filter(|value| value.is_finite())
816 .map(toml::Value::Float)
817 .ok_or_else(|| DocumentError::UnsupportedOperation {
818 format: "TOML".to_string(),
819 operation: "save".to_string(),
820 detail: "float literal is not representable in TOML".to_string(),
821 }),
822 Value::Number(_) => Err(DocumentError::UnsupportedOperation {
823 format: "TOML".to_string(),
824 operation: "save".to_string(),
825 detail: "integer literal exceeds TOML's 64-bit integer range".to_string(),
826 }),
827 Value::String(s) => Ok(toml::Value::String(s.clone())),
828 Value::Array(a) => {
829 let arr = a
830 .iter()
831 .map(our_value_to_toml_value)
832 .collect::<DocumentResult<Vec<_>>>()?;
833 Ok(toml::Value::Array(arr))
834 }
835 Value::Object(o) => {
836 let mut table = toml::map::Map::new();
837 for (k, v) in o {
838 table.insert(k.clone(), our_value_to_toml_value(v)?);
839 }
840 Ok(toml::Value::Table(table))
841 }
842 }
843}