1use crate::{Step, Value};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub enum CommentKind {
24 Head,
26 Inline,
28 Foot,
30}
31
32impl CommentKind {
33 pub fn as_str(self) -> &'static str {
34 match self {
35 CommentKind::Head => "head",
36 CommentKind::Inline => "inline",
37 CommentKind::Foot => "foot",
38 }
39 }
40}
41
42#[derive(Debug, Clone, Default, PartialEq)]
44pub struct Comments {
45 pub head: Vec<String>,
47 pub inline: Option<String>,
49 pub foot: Vec<String>,
51}
52
53impl Comments {
54 pub fn is_empty(&self) -> bool {
55 self.head.is_empty() && self.inline.is_none() && self.foot.is_empty()
56 }
57
58 pub fn get(&self, kind: CommentKind) -> Option<String> {
62 match kind {
63 CommentKind::Head if !self.head.is_empty() => Some(self.head.join(" ")),
64 CommentKind::Foot if !self.foot.is_empty() => Some(self.foot.join(" ")),
65 CommentKind::Inline => self.inline.clone(),
66 _ => None,
67 }
68 }
69}
70
71#[derive(Debug, Clone, PartialEq)]
74pub struct Commented {
75 pub comments: Comments,
76 pub node: CommentedNode,
77}
78
79#[derive(Debug, Clone, PartialEq)]
81pub enum CommentedNode {
82 Scalar(Value),
85 Array(Vec<Commented>),
86 Object(Vec<(String, Commented)>),
87}
88
89impl Commented {
90 pub fn dedupe_keys(&self) -> Commented {
97 let node = match &self.node {
98 CommentedNode::Object(entries) => {
99 let mut seen = std::collections::HashSet::new();
100 CommentedNode::Object(
101 entries
102 .iter()
103 .filter(|(k, _)| seen.insert(k.clone()))
104 .map(|(k, v)| (k.clone(), v.dedupe_keys()))
105 .collect(),
106 )
107 }
108 CommentedNode::Array(items) => {
109 CommentedNode::Array(items.iter().map(Commented::dedupe_keys).collect())
110 }
111 scalar => scalar.clone(),
112 };
113 Commented {
114 comments: self.comments.clone(),
115 node,
116 }
117 }
118
119 pub fn from_value(value: &Value) -> Commented {
121 let node = match value {
122 Value::Array(items) => {
123 CommentedNode::Array(items.iter().map(Commented::from_value).collect())
124 }
125 Value::Object(entries) => CommentedNode::Object(
126 entries
127 .iter()
128 .map(|(k, v)| (k.clone(), Commented::from_value(v)))
129 .collect(),
130 ),
131 scalar => CommentedNode::Scalar(scalar.clone()),
132 };
133 Commented {
134 comments: Comments::default(),
135 node,
136 }
137 }
138
139 pub fn scalar(value: Value) -> Commented {
141 Commented {
142 comments: Comments::default(),
143 node: CommentedNode::Scalar(value),
144 }
145 }
146
147 pub fn to_value(&self) -> Value {
149 match &self.node {
150 CommentedNode::Scalar(v) => v.clone(),
151 CommentedNode::Array(items) => {
152 Value::Array(items.iter().map(Commented::to_value).collect())
153 }
154 CommentedNode::Object(entries) => Value::Object(
155 entries
156 .iter()
157 .map(|(k, v)| (k.clone(), v.to_value()))
158 .collect(),
159 ),
160 }
161 }
162
163 pub fn has_comments(&self) -> bool {
165 if !self.comments.is_empty() {
166 return true;
167 }
168 match &self.node {
169 CommentedNode::Scalar(_) => false,
170 CommentedNode::Array(items) => items.iter().any(Commented::has_comments),
171 CommentedNode::Object(entries) => entries.iter().any(|(_, v)| v.has_comments()),
172 }
173 }
174
175 pub fn attach_trailing_foot(&mut self, lines: Vec<String>) {
178 match &mut self.node {
179 CommentedNode::Object(entries) if !entries.is_empty() => {
180 entries.last_mut().unwrap().1.attach_trailing_foot(lines);
181 }
182 CommentedNode::Array(items) if !items.is_empty() => {
183 items.last_mut().unwrap().attach_trailing_foot(lines);
184 }
185 _ => self.comments.foot.extend(lines),
186 }
187 }
188
189 pub fn descend(&self, path: &[Step]) -> Vec<&Commented> {
196 let mut stream = vec![self];
197 for step in path {
198 let mut next = Vec::new();
199 for node in stream {
200 match (step, &node.node) {
201 (Step::Field(k), CommentedNode::Object(entries)) => {
202 next.extend(entries.iter().find(|(kk, _)| kk == k).map(|(_, v)| v));
203 }
204 (Step::Index(i), CommentedNode::Array(items)) => {
205 let idx = if *i < 0 { items.len() as i64 + i } else { *i };
206 if idx >= 0 && (idx as usize) < items.len() {
207 next.push(&items[idx as usize]);
208 }
209 }
210 (Step::Iterate, CommentedNode::Array(items)) => next.extend(items.iter()),
211 (Step::Iterate, CommentedNode::Object(entries)) => {
212 next.extend(entries.iter().map(|(_, v)| v));
213 }
214 _ => {}
215 }
216 }
217 stream = next;
218 }
219 stream
220 }
221
222 pub fn resolve_comment(&self, path: &[Step]) -> Vec<Value> {
228 let Some((Step::Comment(kind), prefix)) = path.split_last() else {
229 return Vec::new();
230 };
231 self.descend(prefix)
232 .into_iter()
233 .filter_map(|n| n.comments.get(*kind).map(Value::Str))
234 .collect()
235 }
236
237 pub fn comment_targets(&self) -> Vec<(Vec<Step>, crate::CommentKind, String)> {
243 let mut out = Vec::new();
244 collect_targets(self, &mut Vec::new(), &mut out);
245 out
246 }
247}
248
249fn collect_targets(
250 node: &Commented,
251 steps: &mut Vec<Step>,
252 out: &mut Vec<(Vec<Step>, crate::CommentKind, String)>,
253) {
254 use crate::CommentKind::{Foot, Head, Inline};
255 for kind in [Head, Inline, Foot] {
256 if let Some(text) = node.comments.get(kind) {
257 out.push((steps.clone(), kind, text));
258 }
259 }
260 match &node.node {
261 CommentedNode::Scalar(_) => {}
262 CommentedNode::Object(entries) => {
263 for (k, v) in entries {
264 steps.push(Step::Field(k.clone()));
265 collect_targets(v, steps, out);
266 steps.pop();
267 }
268 }
269 CommentedNode::Array(items) => {
270 for (i, v) in items.iter().enumerate() {
271 steps.push(Step::Index(i as i64));
272 collect_targets(v, steps, out);
273 steps.pop();
274 }
275 }
276 }
277}
278
279#[derive(Debug, Clone, Default, PartialEq)]
282pub struct FlatEntry {
283 pub key: String,
284 pub value: String,
285 pub comments: Comments,
286}
287
288pub fn flatten_commented(node: &Commented) -> Vec<FlatEntry> {
296 let mut out = Vec::new();
297 walk("", node, &mut out);
298 out
299}
300
301fn walk(prefix: &str, node: &Commented, out: &mut Vec<FlatEntry>) {
302 match &node.node {
303 CommentedNode::Object(entries) => {
304 let first = out.len();
305 for (k, v) in entries {
306 walk(&join_key(prefix, k), v, out);
307 }
308 distribute_container_comments(node, first, out);
309 }
310 CommentedNode::Array(items) => {
311 let first = out.len();
312 for (i, v) in items.iter().enumerate() {
313 walk(&join_key(prefix, &i.to_string()), v, out);
314 }
315 distribute_container_comments(node, first, out);
316 }
317 CommentedNode::Scalar(v) => out.push(FlatEntry {
318 key: prefix.to_string(),
319 value: v.to_raw_string(),
320 comments: node.comments.clone(),
321 }),
322 }
323}
324
325fn distribute_container_comments(node: &Commented, first: usize, out: &mut [FlatEntry]) {
327 if node.comments.is_empty() || out.len() <= first {
328 return;
329 }
330 let mut head = node.comments.head.clone();
331 head.extend(node.comments.inline.clone());
334 let existing = std::mem::take(&mut out[first].comments.head);
335 head.extend(existing);
336 out[first].comments.head = head;
337 let last = out.len() - 1;
338 out[last].comments.foot.extend(node.comments.foot.clone());
339}
340
341fn join_key(prefix: &str, key: &str) -> String {
342 if prefix.is_empty() {
343 key.to_string()
344 } else {
345 format!("{prefix}.{key}")
346 }
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352
353 fn commented(head: &[&str], inline: Option<&str>, node: CommentedNode) -> Commented {
354 Commented {
355 comments: Comments {
356 head: head.iter().map(|s| s.to_string()).collect(),
357 inline: inline.map(|s| s.to_string()),
358 foot: Vec::new(),
359 },
360 node,
361 }
362 }
363
364 #[test]
365 fn resolve_comment_reads_by_kind() {
366 let tree = Commented {
367 comments: Comments {
368 head: vec!["banner".into()],
369 inline: None,
370 foot: Vec::new(),
371 },
372 node: CommentedNode::Object(vec![
373 (
374 "a".into(),
375 commented(
376 &["one", "two"],
377 Some("why"),
378 CommentedNode::Scalar(Value::Int(1)),
379 ),
380 ),
381 ("b".into(), Commented::scalar(Value::Int(2))),
382 ]),
383 };
384 let head = Step::Comment(CommentKind::Head);
385 let inline = Step::Comment(CommentKind::Inline);
386 assert_eq!(
388 tree.resolve_comment(&[Step::Field("a".into()), head.clone()]),
389 vec![Value::Str("one two".into())]
390 );
391 assert_eq!(
393 tree.resolve_comment(&[Step::Field("a".into()), inline]),
394 vec![Value::Str("why".into())]
395 );
396 assert_eq!(
398 tree.resolve_comment(std::slice::from_ref(&head)),
399 vec![Value::Str("banner".into())]
400 );
401 assert!(
403 tree.resolve_comment(&[Step::Field("b".into()), head.clone()])
404 .is_empty()
405 );
406 assert!(
408 tree.resolve_comment(&[Step::Field("nope".into()), head])
409 .is_empty()
410 );
411 }
412
413 #[test]
414 fn from_value_round_trips_and_is_comment_free() {
415 let v = Value::Object(vec![
416 ("a".into(), Value::Int(1)),
417 ("b".into(), Value::Array(vec![Value::Str("x".into())])),
418 ]);
419 let c = Commented::from_value(&v);
420 assert!(!c.has_comments());
421 assert_eq!(c.to_value(), v);
422 }
423
424 #[test]
425 fn descend_mirrors_eval_paths() {
426 let tree = Commented {
427 comments: Comments::default(),
428 node: CommentedNode::Object(vec![
429 (
430 "a".into(),
431 commented(&["on a"], None, CommentedNode::Scalar(Value::Int(1))),
432 ),
433 (
434 "xs".into(),
435 Commented {
436 comments: Comments::default(),
437 node: CommentedNode::Array(vec![
438 Commented::scalar(Value::Int(10)),
439 commented(&[], Some("last"), CommentedNode::Scalar(Value::Int(20))),
440 ]),
441 },
442 ),
443 ]),
444 };
445 assert_eq!(tree.descend(&[]).len(), 1);
447 let a = tree.descend(&[Step::Field("a".into())]);
449 assert_eq!(a.len(), 1);
450 assert_eq!(a[0].comments.head, vec!["on a"]);
451 let last = tree.descend(&[Step::Field("xs".into()), Step::Index(-1)]);
453 assert_eq!(last[0].comments.inline.as_deref(), Some("last"));
454 assert_eq!(
456 tree.descend(&[Step::Field("xs".into()), Step::Iterate])
457 .len(),
458 2
459 );
460 assert!(tree.descend(&[Step::Field("nope".into())]).is_empty());
462 }
463
464 #[test]
465 fn flatten_carries_comments_to_dotted_keys() {
466 let tree = Commented {
467 comments: Comments {
468 head: vec!["banner".into()],
469 inline: None,
470 foot: vec!["trailer".into()],
471 },
472 node: CommentedNode::Object(vec![(
473 "a".into(),
474 commented(
475 &["section"],
476 None,
477 CommentedNode::Object(vec![(
478 "b".into(),
479 commented(&[], Some("why"), CommentedNode::Scalar(Value::Int(1))),
480 )]),
481 ),
482 )]),
483 };
484 let flat = flatten_commented(&tree);
485 assert_eq!(flat.len(), 1);
486 assert_eq!(flat[0].key, "a.b");
487 assert_eq!(flat[0].value, "1");
488 assert_eq!(flat[0].comments.head, vec!["banner", "section"]);
491 assert_eq!(flat[0].comments.inline.as_deref(), Some("why"));
492 assert_eq!(flat[0].comments.foot, vec!["trailer"]);
493 }
494}