agent_first_data/document/format/
toml.rs1use crate::document::{DocumentError, DocumentResult, Value};
4
5pub fn set_preserving(content: &str, path: &str, value: &Value) -> DocumentResult<String> {
9 let segments = crate::document::parse_path(path)?;
10 if segments.iter().any(|segment| segment.contains(['.', '\\'])) {
11 return Err(DocumentError::UnsupportedOperation {
12 format: "TOML".to_string(),
13 operation: "set".to_string(),
14 detail: "escaped TOML keys are not supported by the current document path adapter"
15 .to_string(),
16 });
17 }
18 let mut document =
19 content
20 .parse::<toml_edit::DocumentMut>()
21 .map_err(|error| DocumentError::ParseError {
22 format: "TOML".to_string(),
23 detail: error.to_string(),
24 })?;
25 let item = toml_item(value)?;
26 let (last, parents) = segments.split_last().ok_or(DocumentError::EmptyPath)?;
27 let mut current = document.as_item_mut();
28 for parent in parents {
29 {
34 let table =
35 current
36 .as_table_like_mut()
37 .ok_or_else(|| DocumentError::UnsupportedOperation {
38 format: "TOML".to_string(),
39 operation: "set".to_string(),
40 detail: "cannot address a key inside a non-table TOML value".to_string(),
41 })?;
42 if table.get(parent).filter(|item| !item.is_none()).is_none() {
43 let mut created = toml_edit::Table::new();
44 created.set_implicit(true);
45 table.insert(parent, toml_edit::Item::Table(created));
46 }
47 }
48 current = current
49 .get_mut(parent)
50 .filter(|item| !item.is_none())
51 .ok_or_else(|| DocumentError::PathNotFound {
52 path: path.to_string(),
53 })?;
54 }
55 let table = current
56 .as_table_like_mut()
57 .ok_or_else(|| DocumentError::UnsupportedOperation {
58 format: "TOML".to_string(),
59 operation: "set".to_string(),
60 detail: "cannot address a key inside a non-table TOML value".to_string(),
61 })?;
62 match table.get_mut(last).filter(|item| !item.is_none()) {
63 Some(target) => {
64 if !target.is_value() {
65 return Err(DocumentError::UnsupportedOperation {
66 format: "TOML".to_string(),
67 operation: "set".to_string(),
68 detail: "only existing scalar TOML values are supported by the document editor"
69 .to_string(),
70 });
71 }
72 let decor = target.as_value().map(|value| value.decor().clone());
73 *target = item;
74 if let (Some(decor), Some(value)) = (decor, target.as_value_mut()) {
75 *value.decor_mut() = decor;
76 }
77 }
78 None => {
81 table.insert(last, item);
82 }
83 }
84 Ok(document.to_string())
85}
86
87pub fn unset_preserving(content: &str, path: &str) -> DocumentResult<String> {
89 let segments = crate::document::parse_path(path)?;
90 if segments.iter().any(|segment| segment.contains(['.', '\\'])) {
91 return Err(DocumentError::UnsupportedOperation {
92 format: "TOML".to_string(),
93 operation: "unset".to_string(),
94 detail: "escaped TOML keys are not supported by the current document path adapter"
95 .to_string(),
96 });
97 }
98 let (last, parents) = segments.split_last().ok_or(DocumentError::EmptyPath)?;
99 let mut document =
100 content
101 .parse::<toml_edit::DocumentMut>()
102 .map_err(|error| DocumentError::ParseError {
103 format: "TOML".to_string(),
104 detail: error.to_string(),
105 })?;
106 let mut current = document.as_item_mut();
107 for parent in parents {
108 current = current
109 .get_mut(parent)
110 .ok_or_else(|| DocumentError::PathNotFound {
111 path: path.to_string(),
112 })?;
113 }
114 let table = current
119 .as_table_like_mut()
120 .ok_or_else(|| DocumentError::UnsupportedOperation {
121 format: "TOML".to_string(),
122 operation: "unset".to_string(),
123 detail: "cannot address a key inside a non-table TOML value".to_string(),
124 })?;
125 let Some(removed) = table.remove(last) else {
126 return Err(DocumentError::PathNotFound {
127 path: path.to_string(),
128 });
129 };
130 let removed_suffix = removed
135 .as_value()
136 .and_then(|value| value.decor().suffix())
137 .cloned();
138 if let (Some(inline), Some(suffix)) = (current.as_inline_table_mut(), removed_suffix)
139 && let Some((_, final_value)) = inline.iter_mut().last()
140 {
141 let carries_its_own = final_value
142 .decor()
143 .suffix()
144 .and_then(|raw| raw.as_str())
145 .is_some_and(|text| !text.is_empty());
146 if !carries_its_own {
147 final_value.decor_mut().set_suffix(suffix);
148 }
149 }
150 Ok(document.to_string())
151}
152
153fn toml_item(value: &Value) -> DocumentResult<toml_edit::Item> {
154 match value {
155 Value::Null => Err(DocumentError::UnsupportedOperation {
156 format: "TOML".to_string(),
157 operation: "set".to_string(),
158 detail: "TOML has no null value".to_string(),
159 }),
160 Value::Bool(value) => Ok(toml_edit::value(*value)),
161 Value::Integer(value) => Ok(toml_edit::value(*value)),
162 Value::Unsigned(value) => i64::try_from(*value).map(toml_edit::value).map_err(|_| {
163 DocumentError::UnsupportedOperation {
164 format: "TOML".to_string(),
165 operation: "set".to_string(),
166 detail: "unsigned integer exceeds TOML i64 range".to_string(),
167 }
168 }),
169 Value::Float(value) if value.is_finite() => Ok(toml_edit::value(*value)),
170 Value::Float(_) => Err(DocumentError::UnsupportedOperation {
171 format: "TOML".to_string(),
172 operation: "set".to_string(),
173 detail: "non-finite TOML float is not representable".to_string(),
174 }),
175 Value::Number(text) if value.is_float() => text
184 .parse::<f64>()
185 .ok()
186 .filter(|value| value.is_finite())
187 .map(toml_edit::value)
188 .ok_or_else(|| DocumentError::UnsupportedOperation {
189 format: "TOML".to_string(),
190 operation: "set".to_string(),
191 detail: format!("float literal `{text}` is not representable in TOML"),
192 }),
193 Value::Number(text) => Err(DocumentError::UnsupportedOperation {
194 format: "TOML".to_string(),
195 operation: "set".to_string(),
196 detail: format!("integer literal `{text}` exceeds TOML's 64-bit integer range"),
197 }),
198 Value::String(value) => Ok(toml_edit::value(value.clone())),
199 Value::Array(_) | Value::Object(_) => Err(DocumentError::UnsupportedOperation {
200 format: "TOML".to_string(),
201 operation: "set".to_string(),
202 detail: "collection mutation requires a dedicated TOML editor".to_string(),
203 }),
204 }
205}
206
207pub fn load(content: &str) -> DocumentResult<Value> {
208 toml::from_str::<toml::Value>(content)
209 .map(value_to_our_value)
210 .map_err(|e| DocumentError::ParseError {
211 format: "TOML".to_string(),
212 detail: e.to_string(),
213 })
214}
215
216pub fn save(value: &Value) -> DocumentResult<String> {
217 let toml_val = our_value_to_toml_value(value)?;
218 toml::to_string_pretty(&toml_val).map_err(|e| DocumentError::ParseError {
219 format: "TOML".to_string(),
220 detail: e.to_string(),
221 })
222}
223
224fn value_to_our_value(v: toml::Value) -> Value {
225 match v {
226 toml::Value::Boolean(b) => Value::Bool(b),
227 toml::Value::Integer(i) => Value::Integer(i),
228 toml::Value::Float(f) => Value::Float(f),
229 toml::Value::String(s) => Value::String(s),
230 toml::Value::Array(a) => Value::Array(a.into_iter().map(value_to_our_value).collect()),
231 toml::Value::Table(t) => {
232 let map = t
233 .into_iter()
234 .map(|(k, v)| (k, value_to_our_value(v)))
235 .collect();
236 Value::Object(map)
237 }
238 toml::Value::Datetime(dt) => Value::String(dt.to_string()),
239 }
240}
241
242fn our_value_to_toml_value(v: &Value) -> DocumentResult<toml::Value> {
243 match v {
244 Value::Null => Err(DocumentError::UnsupportedOperation {
245 format: "TOML".to_string(),
246 operation: "save".to_string(),
247 detail: "TOML has no null value".to_string(),
248 }),
249 Value::Bool(b) => Ok(toml::Value::Boolean(*b)),
250 Value::Integer(i) => Ok(toml::Value::Integer(*i)),
251 Value::Unsigned(i) => i64::try_from(*i).map(toml::Value::Integer).map_err(|_| {
252 DocumentError::UnsupportedOperation {
253 format: "TOML".to_string(),
254 operation: "save".to_string(),
255 detail: "unsigned integer exceeds TOML i64 range".to_string(),
256 }
257 }),
258 Value::Float(f) => Ok(toml::Value::Float(*f)),
259 Value::Number(text) if v.is_float() => text
260 .parse::<f64>()
261 .ok()
262 .filter(|value| value.is_finite())
263 .map(toml::Value::Float)
264 .ok_or_else(|| DocumentError::UnsupportedOperation {
265 format: "TOML".to_string(),
266 operation: "save".to_string(),
267 detail: format!("float literal `{text}` is not representable in TOML"),
268 }),
269 Value::Number(text) => Err(DocumentError::UnsupportedOperation {
270 format: "TOML".to_string(),
271 operation: "save".to_string(),
272 detail: format!("integer literal `{text}` exceeds TOML's 64-bit integer range"),
273 }),
274 Value::String(s) => Ok(toml::Value::String(s.clone())),
275 Value::Array(a) => {
276 let arr = a
277 .iter()
278 .map(our_value_to_toml_value)
279 .collect::<DocumentResult<Vec<_>>>()?;
280 Ok(toml::Value::Array(arr))
281 }
282 Value::Object(o) => {
283 let mut table = toml::map::Map::new();
284 for (k, v) in o {
285 table.insert(k.clone(), our_value_to_toml_value(v)?);
286 }
287 Ok(toml::Value::Table(table))
288 }
289 }
290}