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 from_value(value: &Value) -> Commented {
92 let node = match value {
93 Value::Array(items) => {
94 CommentedNode::Array(items.iter().map(Commented::from_value).collect())
95 }
96 Value::Object(entries) => CommentedNode::Object(
97 entries
98 .iter()
99 .map(|(k, v)| (k.clone(), Commented::from_value(v)))
100 .collect(),
101 ),
102 scalar => CommentedNode::Scalar(scalar.clone()),
103 };
104 Commented {
105 comments: Comments::default(),
106 node,
107 }
108 }
109
110 pub fn scalar(value: Value) -> Commented {
112 Commented {
113 comments: Comments::default(),
114 node: CommentedNode::Scalar(value),
115 }
116 }
117
118 pub fn to_value(&self) -> Value {
120 match &self.node {
121 CommentedNode::Scalar(v) => v.clone(),
122 CommentedNode::Array(items) => {
123 Value::Array(items.iter().map(Commented::to_value).collect())
124 }
125 CommentedNode::Object(entries) => Value::Object(
126 entries
127 .iter()
128 .map(|(k, v)| (k.clone(), v.to_value()))
129 .collect(),
130 ),
131 }
132 }
133
134 pub fn has_comments(&self) -> bool {
136 if !self.comments.is_empty() {
137 return true;
138 }
139 match &self.node {
140 CommentedNode::Scalar(_) => false,
141 CommentedNode::Array(items) => items.iter().any(Commented::has_comments),
142 CommentedNode::Object(entries) => entries.iter().any(|(_, v)| v.has_comments()),
143 }
144 }
145
146 pub fn attach_trailing_foot(&mut self, lines: Vec<String>) {
149 match &mut self.node {
150 CommentedNode::Object(entries) if !entries.is_empty() => {
151 entries.last_mut().unwrap().1.attach_trailing_foot(lines);
152 }
153 CommentedNode::Array(items) if !items.is_empty() => {
154 items.last_mut().unwrap().attach_trailing_foot(lines);
155 }
156 _ => self.comments.foot.extend(lines),
157 }
158 }
159
160 pub fn descend(&self, path: &[Step]) -> Vec<&Commented> {
167 let mut stream = vec![self];
168 for step in path {
169 let mut next = Vec::new();
170 for node in stream {
171 match (step, &node.node) {
172 (Step::Field(k), CommentedNode::Object(entries)) => {
173 next.extend(entries.iter().find(|(kk, _)| kk == k).map(|(_, v)| v));
174 }
175 (Step::Index(i), CommentedNode::Array(items)) => {
176 let idx = if *i < 0 { items.len() as i64 + i } else { *i };
177 if idx >= 0 && (idx as usize) < items.len() {
178 next.push(&items[idx as usize]);
179 }
180 }
181 (Step::Iterate, CommentedNode::Array(items)) => next.extend(items.iter()),
182 (Step::Iterate, CommentedNode::Object(entries)) => {
183 next.extend(entries.iter().map(|(_, v)| v));
184 }
185 _ => {}
186 }
187 }
188 stream = next;
189 }
190 stream
191 }
192
193 pub fn resolve_comment(&self, path: &[Step]) -> Vec<Value> {
199 let Some((Step::Comment(kind), prefix)) = path.split_last() else {
200 return Vec::new();
201 };
202 self.descend(prefix)
203 .into_iter()
204 .filter_map(|n| n.comments.get(*kind).map(Value::Str))
205 .collect()
206 }
207
208 pub fn comment_targets(&self) -> Vec<(Vec<Step>, crate::CommentKind, String)> {
214 let mut out = Vec::new();
215 collect_targets(self, &mut Vec::new(), &mut out);
216 out
217 }
218}
219
220fn collect_targets(
221 node: &Commented,
222 steps: &mut Vec<Step>,
223 out: &mut Vec<(Vec<Step>, crate::CommentKind, String)>,
224) {
225 use crate::CommentKind::{Foot, Head, Inline};
226 for kind in [Head, Inline, Foot] {
227 if let Some(text) = node.comments.get(kind) {
228 out.push((steps.clone(), kind, text));
229 }
230 }
231 match &node.node {
232 CommentedNode::Scalar(_) => {}
233 CommentedNode::Object(entries) => {
234 for (k, v) in entries {
235 steps.push(Step::Field(k.clone()));
236 collect_targets(v, steps, out);
237 steps.pop();
238 }
239 }
240 CommentedNode::Array(items) => {
241 for (i, v) in items.iter().enumerate() {
242 steps.push(Step::Index(i as i64));
243 collect_targets(v, steps, out);
244 steps.pop();
245 }
246 }
247 }
248}
249
250#[derive(Debug, Clone, Default, PartialEq)]
253pub struct FlatEntry {
254 pub key: String,
255 pub value: String,
256 pub comments: Comments,
257}
258
259pub fn flatten_commented(node: &Commented) -> Vec<FlatEntry> {
267 let mut out = Vec::new();
268 walk("", node, &mut out);
269 out
270}
271
272fn walk(prefix: &str, node: &Commented, out: &mut Vec<FlatEntry>) {
273 match &node.node {
274 CommentedNode::Object(entries) => {
275 let first = out.len();
276 for (k, v) in entries {
277 walk(&join_key(prefix, k), v, out);
278 }
279 distribute_container_comments(node, first, out);
280 }
281 CommentedNode::Array(items) => {
282 let first = out.len();
283 for (i, v) in items.iter().enumerate() {
284 walk(&join_key(prefix, &i.to_string()), v, out);
285 }
286 distribute_container_comments(node, first, out);
287 }
288 CommentedNode::Scalar(v) => out.push(FlatEntry {
289 key: prefix.to_string(),
290 value: v.to_raw_string(),
291 comments: node.comments.clone(),
292 }),
293 }
294}
295
296fn distribute_container_comments(node: &Commented, first: usize, out: &mut [FlatEntry]) {
298 if node.comments.is_empty() || out.len() <= first {
299 return;
300 }
301 let mut head = node.comments.head.clone();
302 head.extend(node.comments.inline.clone());
305 let existing = std::mem::take(&mut out[first].comments.head);
306 head.extend(existing);
307 out[first].comments.head = head;
308 let last = out.len() - 1;
309 out[last].comments.foot.extend(node.comments.foot.clone());
310}
311
312fn join_key(prefix: &str, key: &str) -> String {
313 if prefix.is_empty() {
314 key.to_string()
315 } else {
316 format!("{prefix}.{key}")
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323
324 fn commented(head: &[&str], inline: Option<&str>, node: CommentedNode) -> Commented {
325 Commented {
326 comments: Comments {
327 head: head.iter().map(|s| s.to_string()).collect(),
328 inline: inline.map(|s| s.to_string()),
329 foot: Vec::new(),
330 },
331 node,
332 }
333 }
334
335 #[test]
336 fn resolve_comment_reads_by_kind() {
337 let tree = Commented {
338 comments: Comments {
339 head: vec!["banner".into()],
340 inline: None,
341 foot: Vec::new(),
342 },
343 node: CommentedNode::Object(vec![
344 (
345 "a".into(),
346 commented(
347 &["one", "two"],
348 Some("why"),
349 CommentedNode::Scalar(Value::Int(1)),
350 ),
351 ),
352 ("b".into(), Commented::scalar(Value::Int(2))),
353 ]),
354 };
355 let head = Step::Comment(CommentKind::Head);
356 let inline = Step::Comment(CommentKind::Inline);
357 assert_eq!(
359 tree.resolve_comment(&[Step::Field("a".into()), head.clone()]),
360 vec![Value::Str("one two".into())]
361 );
362 assert_eq!(
364 tree.resolve_comment(&[Step::Field("a".into()), inline]),
365 vec![Value::Str("why".into())]
366 );
367 assert_eq!(
369 tree.resolve_comment(std::slice::from_ref(&head)),
370 vec![Value::Str("banner".into())]
371 );
372 assert!(
374 tree.resolve_comment(&[Step::Field("b".into()), head.clone()])
375 .is_empty()
376 );
377 assert!(
379 tree.resolve_comment(&[Step::Field("nope".into()), head])
380 .is_empty()
381 );
382 }
383
384 #[test]
385 fn from_value_round_trips_and_is_comment_free() {
386 let v = Value::Object(vec![
387 ("a".into(), Value::Int(1)),
388 ("b".into(), Value::Array(vec![Value::Str("x".into())])),
389 ]);
390 let c = Commented::from_value(&v);
391 assert!(!c.has_comments());
392 assert_eq!(c.to_value(), v);
393 }
394
395 #[test]
396 fn descend_mirrors_eval_paths() {
397 let tree = Commented {
398 comments: Comments::default(),
399 node: CommentedNode::Object(vec![
400 (
401 "a".into(),
402 commented(&["on a"], None, CommentedNode::Scalar(Value::Int(1))),
403 ),
404 (
405 "xs".into(),
406 Commented {
407 comments: Comments::default(),
408 node: CommentedNode::Array(vec![
409 Commented::scalar(Value::Int(10)),
410 commented(&[], Some("last"), CommentedNode::Scalar(Value::Int(20))),
411 ]),
412 },
413 ),
414 ]),
415 };
416 assert_eq!(tree.descend(&[]).len(), 1);
418 let a = tree.descend(&[Step::Field("a".into())]);
420 assert_eq!(a.len(), 1);
421 assert_eq!(a[0].comments.head, vec!["on a"]);
422 let last = tree.descend(&[Step::Field("xs".into()), Step::Index(-1)]);
424 assert_eq!(last[0].comments.inline.as_deref(), Some("last"));
425 assert_eq!(
427 tree.descend(&[Step::Field("xs".into()), Step::Iterate])
428 .len(),
429 2
430 );
431 assert!(tree.descend(&[Step::Field("nope".into())]).is_empty());
433 }
434
435 #[test]
436 fn flatten_carries_comments_to_dotted_keys() {
437 let tree = Commented {
438 comments: Comments {
439 head: vec!["banner".into()],
440 inline: None,
441 foot: vec!["trailer".into()],
442 },
443 node: CommentedNode::Object(vec![(
444 "a".into(),
445 commented(
446 &["section"],
447 None,
448 CommentedNode::Object(vec![(
449 "b".into(),
450 commented(&[], Some("why"), CommentedNode::Scalar(Value::Int(1))),
451 )]),
452 ),
453 )]),
454 };
455 let flat = flatten_commented(&tree);
456 assert_eq!(flat.len(), 1);
457 assert_eq!(flat[0].key, "a.b");
458 assert_eq!(flat[0].value, "1");
459 assert_eq!(flat[0].comments.head, vec!["banner", "section"]);
462 assert_eq!(flat[0].comments.inline.as_deref(), Some("why"));
463 assert_eq!(flat[0].comments.foot, vec!["trailer"]);
464 }
465}