agent_first_data/document/format/
toml.rs1use crate::document::{DocumentError, DocumentResult, Value};
4
5pub fn set_scalar_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 current = current
32 .get_mut(parent)
33 .filter(|item| !item.is_none())
34 .ok_or_else(|| DocumentError::PathNotFound {
35 path: path.to_string(),
36 })?;
37 }
38 let table = current
39 .as_table_like_mut()
40 .ok_or_else(|| DocumentError::UnsupportedOperation {
41 format: "TOML".to_string(),
42 operation: "set".to_string(),
43 detail: "cannot address a key inside a non-table TOML value".to_string(),
44 })?;
45 match table.get_mut(last).filter(|item| !item.is_none()) {
46 Some(target) => {
47 if !target.is_value() {
48 return Err(DocumentError::UnsupportedOperation {
49 format: "TOML".to_string(),
50 operation: "set".to_string(),
51 detail: "only existing scalar TOML values are supported by the document editor"
52 .to_string(),
53 });
54 }
55 let decor = target.as_value().map(|value| value.decor().clone());
56 *target = item;
57 if let (Some(decor), Some(value)) = (decor, target.as_value_mut()) {
58 *value.decor_mut() = decor;
59 }
60 }
61 None => {
64 table.insert(last, item);
65 }
66 }
67 Ok(document.to_string())
68}
69
70pub fn unset_preserving(content: &str, path: &str) -> DocumentResult<String> {
72 let segments = crate::document::parse_path(path)?;
73 if segments.iter().any(|segment| segment.contains(['.', '\\'])) {
74 return Err(DocumentError::UnsupportedOperation {
75 format: "TOML".to_string(),
76 operation: "unset".to_string(),
77 detail: "escaped TOML keys are not supported by the current document path adapter"
78 .to_string(),
79 });
80 }
81 let (last, parents) = segments.split_last().ok_or(DocumentError::EmptyPath)?;
82 let mut document =
83 content
84 .parse::<toml_edit::DocumentMut>()
85 .map_err(|error| DocumentError::ParseError {
86 format: "TOML".to_string(),
87 detail: error.to_string(),
88 })?;
89 let mut current = document.as_item_mut();
90 for parent in parents {
91 current = current
92 .get_mut(parent)
93 .ok_or_else(|| DocumentError::PathNotFound {
94 path: path.to_string(),
95 })?;
96 }
97 let table = current
98 .as_table_mut()
99 .ok_or_else(|| DocumentError::UnsupportedOperation {
100 format: "TOML".to_string(),
101 operation: "unset".to_string(),
102 detail: "only table entries can be removed by the current TOML editor".to_string(),
103 })?;
104 if table.remove(last).is_none() {
105 return Err(DocumentError::PathNotFound {
106 path: path.to_string(),
107 });
108 }
109 Ok(document.to_string())
110}
111
112fn toml_item(value: &Value) -> DocumentResult<toml_edit::Item> {
113 match value {
114 Value::Null => Err(DocumentError::UnsupportedOperation {
115 format: "TOML".to_string(),
116 operation: "set".to_string(),
117 detail: "TOML has no null value".to_string(),
118 }),
119 Value::Bool(value) => Ok(toml_edit::value(*value)),
120 Value::Integer(value) => Ok(toml_edit::value(*value)),
121 Value::Unsigned(value) => i64::try_from(*value).map(toml_edit::value).map_err(|_| {
122 DocumentError::UnsupportedOperation {
123 format: "TOML".to_string(),
124 operation: "set".to_string(),
125 detail: "unsigned integer exceeds TOML i64 range".to_string(),
126 }
127 }),
128 Value::Float(value) if value.is_finite() => Ok(toml_edit::value(*value)),
129 Value::Float(_) => Err(DocumentError::UnsupportedOperation {
130 format: "TOML".to_string(),
131 operation: "set".to_string(),
132 detail: "non-finite TOML float is not representable".to_string(),
133 }),
134 Value::Number(text) if value.is_float() => text
143 .parse::<f64>()
144 .ok()
145 .filter(|value| value.is_finite())
146 .map(toml_edit::value)
147 .ok_or_else(|| DocumentError::UnsupportedOperation {
148 format: "TOML".to_string(),
149 operation: "set".to_string(),
150 detail: format!("float literal `{text}` is not representable in TOML"),
151 }),
152 Value::Number(text) => Err(DocumentError::UnsupportedOperation {
153 format: "TOML".to_string(),
154 operation: "set".to_string(),
155 detail: format!("integer literal `{text}` exceeds TOML's 64-bit integer range"),
156 }),
157 Value::String(value) => Ok(toml_edit::value(value.clone())),
158 Value::Array(_) | Value::Object(_) => Err(DocumentError::UnsupportedOperation {
159 format: "TOML".to_string(),
160 operation: "set".to_string(),
161 detail: "collection mutation requires a dedicated TOML editor".to_string(),
162 }),
163 }
164}
165
166pub fn load(content: &str) -> DocumentResult<Value> {
167 toml::from_str::<toml::Value>(content)
168 .map(value_to_our_value)
169 .map_err(|e| DocumentError::ParseError {
170 format: "TOML".to_string(),
171 detail: e.to_string(),
172 })
173}
174
175pub fn save(value: &Value) -> DocumentResult<String> {
176 let toml_val = our_value_to_toml_value(value)?;
177 toml::to_string_pretty(&toml_val).map_err(|e| DocumentError::ParseError {
178 format: "TOML".to_string(),
179 detail: e.to_string(),
180 })
181}
182
183fn value_to_our_value(v: toml::Value) -> Value {
184 match v {
185 toml::Value::Boolean(b) => Value::Bool(b),
186 toml::Value::Integer(i) => Value::Integer(i),
187 toml::Value::Float(f) => Value::Float(f),
188 toml::Value::String(s) => Value::String(s),
189 toml::Value::Array(a) => Value::Array(a.into_iter().map(value_to_our_value).collect()),
190 toml::Value::Table(t) => {
191 let map = t
192 .into_iter()
193 .map(|(k, v)| (k, value_to_our_value(v)))
194 .collect();
195 Value::Object(map)
196 }
197 toml::Value::Datetime(dt) => Value::String(dt.to_string()),
198 }
199}
200
201fn our_value_to_toml_value(v: &Value) -> DocumentResult<toml::Value> {
202 match v {
203 Value::Null => Err(DocumentError::UnsupportedOperation {
204 format: "TOML".to_string(),
205 operation: "save".to_string(),
206 detail: "TOML has no null value".to_string(),
207 }),
208 Value::Bool(b) => Ok(toml::Value::Boolean(*b)),
209 Value::Integer(i) => Ok(toml::Value::Integer(*i)),
210 Value::Unsigned(i) => i64::try_from(*i).map(toml::Value::Integer).map_err(|_| {
211 DocumentError::UnsupportedOperation {
212 format: "TOML".to_string(),
213 operation: "save".to_string(),
214 detail: "unsigned integer exceeds TOML i64 range".to_string(),
215 }
216 }),
217 Value::Float(f) => Ok(toml::Value::Float(*f)),
218 Value::Number(text) if v.is_float() => text
219 .parse::<f64>()
220 .ok()
221 .filter(|value| value.is_finite())
222 .map(toml::Value::Float)
223 .ok_or_else(|| DocumentError::UnsupportedOperation {
224 format: "TOML".to_string(),
225 operation: "save".to_string(),
226 detail: format!("float literal `{text}` is not representable in TOML"),
227 }),
228 Value::Number(text) => Err(DocumentError::UnsupportedOperation {
229 format: "TOML".to_string(),
230 operation: "save".to_string(),
231 detail: format!("integer literal `{text}` exceeds TOML's 64-bit integer range"),
232 }),
233 Value::String(s) => Ok(toml::Value::String(s.clone())),
234 Value::Array(a) => {
235 let arr = a
236 .iter()
237 .map(our_value_to_toml_value)
238 .collect::<DocumentResult<Vec<_>>>()?;
239 Ok(toml::Value::Array(arr))
240 }
241 Value::Object(o) => {
242 let mut table = toml::map::Map::new();
243 for (k, v) in o {
244 table.insert(k.clone(), our_value_to_toml_value(v)?);
245 }
246 Ok(toml::Value::Table(table))
247 }
248 }
249}