yerba 0.5.1

YAML Editing and Refactoring with Better Accuracy
use yaml_parser::ast::BlockSeq;

use rowan::ast::AstNode;

use crate::document::{extract_scalar_text, navigate_from_node, Document};
use crate::error::YerbaError;

#[derive(Debug, Clone)]
pub struct DuplicateInfo {
  pub value: String,
  pub line: usize,
}

impl Document {
  pub fn unique(&mut self, dot_path: &str, by: &str, remove: bool) -> Result<Vec<DuplicateInfo>, YerbaError> {
    self.unique_with_options(dot_path, by, remove, false)
  }

  pub fn unique_with_options(&mut self, dot_path: &str, by: &str, remove: bool, allow_blank_duplicates: bool) -> Result<Vec<DuplicateInfo>, YerbaError> {
    let current_node = self.navigate(dot_path)?;
    let source = self.root.text().to_string();

    let sequence = match current_node.descendants().find_map(BlockSeq::cast) {
      Some(sequence) => sequence,
      None => return Ok(Vec::new()),
    };

    let entries: Vec<_> = sequence.entries().collect();

    if entries.len() <= 1 {
      return Ok(Vec::new());
    }

    let by_is_scalar = by == ".";

    let labels: Vec<(Option<String>, usize)> = entries
      .iter()
      .map(|entry| {
        let offset: usize = entry.syntax().text_range().start().into();
        let line = source[..offset].matches('\n').count() + 1;

        let value = if by_is_scalar {
          Some(entry.flow().and_then(|flow| extract_scalar_text(flow.syntax())).unwrap_or_default())
        } else {
          let field = by.strip_prefix('.').unwrap_or(by);
          let nodes = navigate_from_node(entry.syntax(), field);

          nodes.first().and_then(extract_scalar_text)
        };

        (value, line)
      })
      .collect();

    let mut seen = std::collections::HashSet::new();
    let mut duplicate_indices: Vec<usize> = Vec::new();
    let mut duplicates: Vec<DuplicateInfo> = Vec::new();

    for (index, (label, line)) in labels.iter().enumerate() {
      let label = match label {
        None => continue,
        Some(value) => value,
      };

      if allow_blank_duplicates && label.is_empty() {
        continue;
      }

      if !seen.insert(label.clone()) {
        duplicate_indices.push(index);
        duplicates.push(DuplicateInfo {
          value: label.clone(),
          line: *line,
        });
      }
    }

    if remove && !duplicate_indices.is_empty() {
      for &index in duplicate_indices.iter().rev() {
        self.remove_at(dot_path, index)?;
      }
    }

    Ok(duplicates)
  }
}