agent_first_data/document/format/
yaml.rs1use crate::document::{DocumentError, DocumentResult, Value};
6use noyalib::{
7 DuplicateKeyPolicy, Mapping as YamlMapping, ParserConfig, Value as YamlValue,
8 cst::parse_document, from_str_with_config, to_string,
9};
10
11pub fn load(content: &str) -> DocumentResult<Value> {
12 let parser_config = ParserConfig::new()
13 .duplicate_key_policy(DuplicateKeyPolicy::Error)
14 .lossless_u64_integers(true);
15 from_str_with_config::<YamlValue>(content, &parser_config)
16 .map(value_to_our_value)
17 .map_err(|e| DocumentError::ParseError {
18 format: "YAML".to_string(),
19 detail: e.to_string(),
20 })
21}
22
23pub fn set_scalar_preserving(content: &str, path: &str, value: &Value) -> DocumentResult<String> {
27 let segments = crate::document::parse_path(path)?;
28 let yaml_path = cst_path(&segments, "set")?;
29 if !matches!(
30 value,
31 Value::Null
32 | Value::Bool(_)
33 | Value::Integer(_)
34 | Value::Unsigned(_)
35 | Value::Float(_)
36 | Value::String(_)
37 ) {
38 return Err(DocumentError::UnsupportedOperation {
39 format: "YAML".to_string(),
40 operation: "set".to_string(),
41 detail: "collection mutation requires a dedicated CST fragment editor".to_string(),
42 });
43 }
44 let mut document = parse_document(content).map_err(|error| DocumentError::ParseError {
45 format: "YAML".to_string(),
46 detail: error.to_string(),
47 })?;
48 let exists = load(content)
51 .ok()
52 .is_some_and(|loaded| crate::document::get_path_ref(&loaded, path, &[]).is_ok());
53 if exists {
54 document
55 .set_value(&yaml_path, &to_noyalib_value(value)?)
56 .map_err(|error| DocumentError::UnsupportedOperation {
57 format: "YAML".to_string(),
58 operation: "set".to_string(),
59 detail: error.to_string(),
60 })?;
61 } else {
62 let (last, parents) = segments.split_last().ok_or(DocumentError::EmptyPath)?;
63 if last.parse::<usize>().is_ok() {
64 return Err(DocumentError::UnsupportedOperation {
65 format: "YAML".to_string(),
66 operation: "set".to_string(),
67 detail: "cannot create a new sequence index; the element must already exist"
68 .to_string(),
69 });
70 }
71 let parent_path = if parents.is_empty() {
72 String::new()
73 } else {
74 cst_path(parents, "set")?
75 };
76 let fragment = to_string(&to_noyalib_value(value)?).map_err(|error| {
77 DocumentError::UnsupportedOperation {
78 format: "YAML".to_string(),
79 operation: "set".to_string(),
80 detail: error.to_string(),
81 }
82 })?;
83 document
84 .insert_entry(&parent_path, last, fragment.trim_end())
85 .map_err(|error| DocumentError::UnsupportedOperation {
86 format: "YAML".to_string(),
87 operation: "set".to_string(),
88 detail: error.to_string(),
89 })?;
90 }
91 document
92 .validate()
93 .map_err(|error| DocumentError::ParseError {
94 format: "YAML".to_string(),
95 detail: error.to_string(),
96 })?;
97 Ok(document.to_string())
98}
99
100pub fn unset_preserving(content: &str, path: &str) -> DocumentResult<String> {
102 let segments = crate::document::parse_path(path)?;
103 let yaml_path = cst_path(&segments, "unset")?;
104 let mut document = parse_document(content).map_err(|error| DocumentError::ParseError {
105 format: "YAML".to_string(),
106 detail: error.to_string(),
107 })?;
108 document
109 .remove(&yaml_path)
110 .map_err(|error| DocumentError::UnsupportedOperation {
111 format: "YAML".to_string(),
112 operation: "unset".to_string(),
113 detail: error.to_string(),
114 })?;
115 document
116 .validate()
117 .map_err(|error| DocumentError::ParseError {
118 format: "YAML".to_string(),
119 detail: error.to_string(),
120 })?;
121 Ok(document.to_string())
122}
123
124pub fn append_array_item_preserving(
127 content: &str,
128 path: &str,
129 item: &Value,
130) -> DocumentResult<String> {
131 let mut document = parse_document(content).map_err(|error| DocumentError::ParseError {
132 format: "YAML".to_string(),
133 detail: error.to_string(),
134 })?;
135 let fragment = to_string(&to_noyalib_value(item)?).map_err(|error| {
136 DocumentError::UnsupportedOperation {
137 format: "YAML".to_string(),
138 operation: "add".to_string(),
139 detail: error.to_string(),
140 }
141 })?;
142 let yaml_path = cst_path(&crate::document::parse_path(path)?, "add")?;
143 document
144 .push_back(&yaml_path, fragment.trim_end())
145 .map_err(|error| DocumentError::UnsupportedOperation {
146 format: "YAML".to_string(),
147 operation: "add".to_string(),
148 detail: error.to_string(),
149 })?;
150 document
151 .validate()
152 .map_err(|error| DocumentError::ParseError {
153 format: "YAML".to_string(),
154 detail: error.to_string(),
155 })?;
156 Ok(document.to_string())
157}
158
159pub fn remove_array_item_preserving(
161 content: &str,
162 path: &str,
163 index: usize,
164) -> DocumentResult<String> {
165 let mut document = parse_document(content).map_err(|error| DocumentError::ParseError {
166 format: "YAML".to_string(),
167 detail: error.to_string(),
168 })?;
169 let yaml_path = cst_path(&crate::document::parse_path(path)?, "remove")?;
170 document
171 .remove(&format!("{yaml_path}[{index}]"))
172 .map_err(|error| DocumentError::UnsupportedOperation {
173 format: "YAML".to_string(),
174 operation: "remove".to_string(),
175 detail: error.to_string(),
176 })?;
177 document
178 .validate()
179 .map_err(|error| DocumentError::ParseError {
180 format: "YAML".to_string(),
181 detail: error.to_string(),
182 })?;
183 Ok(document.to_string())
184}
185
186fn cst_path(segments: &[String], operation: &str) -> DocumentResult<String> {
187 if segments.is_empty() {
188 return Err(DocumentError::UnsupportedOperation {
189 format: "YAML".to_string(),
190 operation: operation.to_string(),
191 detail: "root mutation is not supported by the CST path adapter".to_string(),
192 });
193 }
194 let mut path = String::new();
195 for segment in segments {
196 if segment.contains(['.', '\\']) {
197 return Err(DocumentError::UnsupportedOperation {
198 format: "YAML".to_string(),
199 operation: operation.to_string(),
200 detail: "escaped YAML keys require a quoted-key CST span and are not supported"
201 .to_string(),
202 });
203 }
204 if let Ok(index) = segment.parse::<usize>() {
205 path.push_str(&format!("[{index}]"));
206 } else {
207 if !path.is_empty() {
208 path.push('.');
209 }
210 path.push_str(segment);
211 }
212 }
213 Ok(path)
214}
215
216fn to_noyalib_value(value: &Value) -> DocumentResult<YamlValue> {
217 match value {
218 Value::Null => Ok(YamlValue::Null),
219 Value::Bool(value) => Ok(YamlValue::Bool(*value)),
220 Value::Integer(value) => Ok(YamlValue::from(*value)),
221 Value::Unsigned(value) => Ok(YamlValue::from(*value)),
222 Value::Float(value) if value.is_finite() => Ok(YamlValue::from(*value)),
223 Value::Float(_) => Err(DocumentError::UnsupportedOperation {
224 format: "YAML".to_string(),
225 operation: "set".to_string(),
226 detail: "non-finite YAML float is not representable".to_string(),
227 }),
228 Value::String(value) => Ok(YamlValue::String(value.clone())),
229 Value::Array(values) => Ok(YamlValue::Sequence(
230 values
231 .iter()
232 .map(to_noyalib_value)
233 .collect::<DocumentResult<Vec<_>>>()?,
234 )),
235 Value::Object(values) => {
236 let mut mapping = YamlMapping::new();
237 for (key, value) in values {
238 mapping.insert(key.clone(), to_noyalib_value(value)?);
239 }
240 Ok(YamlValue::Mapping(mapping))
241 }
242 }
243}
244
245pub fn save(value: &Value) -> DocumentResult<String> {
246 let yaml_val = our_value_to_yaml_value(value)?;
247 to_string(&yaml_val).map_err(|e| DocumentError::ParseError {
248 format: "YAML".to_string(),
249 detail: e.to_string(),
250 })
251}
252
253fn value_to_our_value(v: YamlValue) -> Value {
254 match v {
255 YamlValue::Null => Value::Null,
256 YamlValue::Bool(b) => Value::Bool(b),
257 YamlValue::Number(n) => {
258 if let Some(i) = n.as_i64() {
259 Value::Integer(i)
260 } else if let Some(u) = n.as_u64() {
261 Value::Unsigned(u)
262 } else {
263 Value::Float(n.as_f64())
264 }
265 }
266 YamlValue::String(s) => Value::String(s),
267 YamlValue::Sequence(seq) => Value::Array(seq.into_iter().map(value_to_our_value).collect()),
268 YamlValue::Mapping(map) => {
269 let mut obj = std::collections::BTreeMap::new();
270 for (key, value) in map {
271 obj.insert(key, value_to_our_value(value));
272 }
273 Value::Object(obj)
274 }
275 YamlValue::Tagged(t) => {
276 let (_, value) = t.into_parts();
278 value_to_our_value(value)
279 }
280 }
281}
282
283fn our_value_to_yaml_value(v: &Value) -> DocumentResult<YamlValue> {
284 match v {
285 Value::Null => Ok(YamlValue::Null),
286 Value::Bool(b) => Ok(YamlValue::Bool(*b)),
287 Value::Integer(i) => Ok(YamlValue::Number((*i).into())),
288 Value::Unsigned(i) => Ok(YamlValue::Number((*i).into())),
289 Value::Float(f) => {
290 Ok(YamlValue::Number((*f).into()))
292 }
293 Value::String(s) => Ok(YamlValue::String(s.clone())),
294 Value::Array(a) => {
295 let seq = a
296 .iter()
297 .map(our_value_to_yaml_value)
298 .collect::<DocumentResult<Vec<_>>>()?;
299 Ok(YamlValue::Sequence(seq))
300 }
301 Value::Object(o) => {
302 let mut mapping = YamlMapping::new();
303 for (k, v) in o {
304 mapping.insert(k.clone(), our_value_to_yaml_value(v)?);
305 }
306 Ok(YamlValue::Mapping(mapping))
307 }
308 }
309}