yerba 0.5.1

YAML Editing and Refactoring with Better Accuracy
#[derive(Debug)]
pub enum YerbaError {
  ParseError(String),
  IoError(std::io::Error),
  SelectorNotFound(String),
  AmbiguousSelector(String, usize),
  NotASequence(String),
  IndexOutOfBounds(usize, usize),
  UnknownKeys(Vec<String>),
  DuplicateValues(Vec<crate::DuplicateInfo>),
  SchemaValidation(Vec<crate::schema::ValidationError>),
  DuplicateKey {
    key: String,
    first_line: usize,
    duplicate_line: usize,
    line_content: String,
  },
}

impl std::fmt::Display for YerbaError {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    match self {
      YerbaError::ParseError(msg) => write!(f, "parse error: {}", msg),
      YerbaError::IoError(err) => write!(f, "io error: {}", err),
      YerbaError::SelectorNotFound(selector) => write!(f, "selector not found: {}", selector),
      YerbaError::NotASequence(path) => write!(f, "not a sequence: {}", path),

      YerbaError::AmbiguousSelector(selector, count) => {
        write!(
          f,
          "selector \"{}\" matched {} nodes (expected 1). Use --all to update all matches",
          selector, count
        )
      }

      YerbaError::DuplicateKey {
        key,
        first_line,
        duplicate_line,
        line_content,
      } => {
        write!(
          f,
          "duplicate key \"{}\" on line {} (first defined on line {})\n\n    {} | {}",
          key,
          duplicate_line,
          first_line,
          duplicate_line,
          line_content.trim()
        )
      }

      YerbaError::DuplicateValues(duplicates) => {
        let noun = if duplicates.len() == 1 { "duplicate" } else { "duplicates" };
        let details: Vec<String> = duplicates
          .iter()
          .map(|duplicate| format!("\"{}\" (line {})", duplicate.value, duplicate.line))
          .collect();

        write!(f, "found {} {}: {}", duplicates.len(), noun, details.join(", "))
      }

      YerbaError::IndexOutOfBounds(index, length) => {
        write!(f, "index {} out of bounds (length {})", index, length)
      }

      YerbaError::SchemaValidation(errors) => {
        let details: Vec<String> = errors.iter().map(|error| error.to_string()).collect();

        write!(f, "schema validation failed:\n{}", details.join("\n"))
      }

      YerbaError::UnknownKeys(keys) => {
        let suggestion = keys.iter().map(|key| format!("\"{}\"", key)).collect::<Vec<_>>().join(", ");

        write!(
          f,
          "found keys not listed in sort order: {}\n\n  Add them to your sort order or Yerbafile:\n    {}\n",
          keys.join(", "),
          suggestion
        )
      }
    }
  }
}

impl From<std::io::Error> for YerbaError {
  fn from(err: std::io::Error) -> Self {
    YerbaError::IoError(err)
  }
}

pub struct GitHubAnnotation {
  pub level: &'static str,
  pub file: String,
  pub line: Option<usize>,
  pub message: String,
}

impl std::fmt::Display for GitHubAnnotation {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    match self.line {
      Some(line) => write!(f, "::{}file={},line={}::{}", self.level, self.file, line, self.message),
      None => write!(f, "::{}file={}::{}", self.level, self.file, self.message),
    }
  }
}

pub trait GitHubAnnotations {
  fn github_annotations(&self, file: &str) -> Vec<GitHubAnnotation>;
}

impl GitHubAnnotations for YerbaError {
  fn github_annotations(&self, file: &str) -> Vec<GitHubAnnotation> {
    match self {
      YerbaError::DuplicateValues(duplicates) => duplicates
        .iter()
        .map(|duplicate| GitHubAnnotation {
          level: "error ",
          file: file.to_string(),
          line: Some(duplicate.line),
          message: format!("duplicate: \"{}\"", duplicate.value),
        })
        .collect(),

      YerbaError::SchemaValidation(errors) => errors
        .iter()
        .map(|error| GitHubAnnotation {
          level: "error ",
          file: file.to_string(),
          line: error.line,
          message: error.to_string(),
        })
        .collect(),

      YerbaError::DuplicateKey { key, duplicate_line, .. } => vec![GitHubAnnotation {
        level: "error ",
        file: file.to_string(),
        line: Some(*duplicate_line),
        message: format!("duplicate key: \"{}\"", key),
      }],

      _ => vec![GitHubAnnotation {
        level: "error ",
        file: file.to_string(),
        line: None,
        message: self.to_string(),
      }],
    }
  }
}