yerba 0.4.2

YAML Editing and Refactoring with Better Accuracy
use super::*;

impl Document {
  pub fn append(&mut self, dot_path: &str, value: &str) -> Result<(), YerbaError> {
    self.insert_into(dot_path, value, InsertPosition::Last)
  }

  pub fn detect_sequence_quote_style(&self, dot_path: &str) -> QuoteStyle {
    let try_paths = if dot_path.is_empty() {
      vec!["[]".to_string(), "[0]".to_string()]
    } else {
      vec![format!("{}[]", dot_path), format!("{}[0]", dot_path)]
    };

    for try_path in &try_paths {
      for scalar in self.get_all_typed(try_path) {
        if scalar.kind == SyntaxKind::DOUBLE_QUOTED_SCALAR {
          return QuoteStyle::Double;
        } else if scalar.kind == SyntaxKind::SINGLE_QUOTED_SCALAR {
          return QuoteStyle::Single;
        }
      }
    }

    if let Some(serde_yaml::Value::Sequence(sequence)) = self.get_value(dot_path).as_ref() {
      if let Some(serde_yaml::Value::Mapping(map)) = sequence.first() {
        if let Some((serde_yaml::Value::String(key_name), _)) = map.iter().next() {
          let deep_path = if dot_path.is_empty() {
            format!("[].{}", key_name)
          } else {
            format!("{}[].{}", dot_path, key_name)
          };

          for scalar in self.get_all_typed(&deep_path) {
            if scalar.kind == SyntaxKind::DOUBLE_QUOTED_SCALAR {
              return QuoteStyle::Double;
            } else if scalar.kind == SyntaxKind::SINGLE_QUOTED_SCALAR {
              return QuoteStyle::Single;
            }
          }
        }
      }
    }

    QuoteStyle::Plain
  }

  pub fn insert_object(&mut self, dot_path: &str, json_value: &serde_json::Value, position: InsertPosition) -> Result<(), YerbaError> {
    let quote_style = self.detect_sequence_quote_style(dot_path);
    let yaml_text = crate::yaml_writer::json_to_yaml_text(json_value, &quote_style, 0);

    self.insert_into(dot_path, &yaml_text, position)
  }

  pub fn insert_objects(&mut self, dot_path: &str, json_values: &[serde_json::Value]) -> Result<(), YerbaError> {
    if json_values.is_empty() {
      return Ok(());
    }

    let quote_style = self.detect_sequence_quote_style(dot_path);
    let current_node = self.navigate(dot_path)?;

    let sequence = current_node
      .descendants()
      .find_map(BlockSeq::cast)
      .ok_or_else(|| YerbaError::NotASequence(dot_path.to_string()))?;

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

    if entries.is_empty() {
      return Err(YerbaError::SelectorNotFound(dot_path.to_string()));
    }

    let indent = entries
      .get(1)
      .or(entries.first())
      .map(|entry| preceding_whitespace_indent(entry.syntax()))
      .unwrap_or_default();

    let mut new_text = String::new();

    for json_value in json_values {
      let yaml_text = crate::yaml_writer::json_to_yaml_text(json_value, &quote_style, 0);

      let new_item = if yaml_text.contains('\n') {
        let item_indent = format!("{}  ", indent);
        let lines: Vec<&str> = yaml_text.split('\n').collect();

        let min_indent = lines
          .iter()
          .skip(1)
          .filter(|line| !line.trim().is_empty())
          .map(|line| line.len() - line.trim_start().len())
          .min()
          .unwrap_or(0);

        let indented: Vec<String> = lines
          .iter()
          .enumerate()
          .map(|(index, line)| {
            if index == 0 {
              line.to_string()
            } else if line.trim().is_empty() {
              String::new()
            } else {
              let relative = &line[min_indent..];
              format!("{}{}", item_indent, relative)
            }
          })
          .collect();

        format!("- {}", indented.join("\n"))
      } else {
        format!("- {}", yaml_text)
      };

      new_text.push_str(&format!("\n{}{}", indent, new_item));
    }

    let last_entry = entries.last().unwrap();

    self.insert_after_node(last_entry.syntax(), &new_text)
  }

  pub fn insert_into(&mut self, dot_path: &str, value: &str, position: InsertPosition) -> Result<(), YerbaError> {
    Self::validate_path(dot_path)?;

    if let Ok(current_node) = self.navigate(dot_path) {
      if current_node.descendants().find_map(BlockSeq::cast).is_some() {
        return self.insert_sequence_item(dot_path, value, position);
      }
    }

    let (parent_path, key) = dot_path.rsplit_once('.').unwrap_or(("", dot_path));

    self.insert_map_key(parent_path, key, value, position)
  }

  fn insert_sequence_item(&mut self, dot_path: &str, value: &str, position: InsertPosition) -> Result<(), YerbaError> {
    let current_node = self.navigate(dot_path)?;

    let sequence = current_node
      .descendants()
      .find_map(BlockSeq::cast)
      .ok_or_else(|| YerbaError::NotASequence(dot_path.to_string()))?;

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

    if entries.is_empty() {
      return Err(YerbaError::SelectorNotFound(dot_path.to_string()));
    }

    let indent = entries
      .get(1)
      .or(entries.first())
      .map(|entry| preceding_whitespace_indent(entry.syntax()))
      .unwrap_or_default();

    let new_item = if value.contains('\n') {
      let item_indent = format!("{}  ", indent);
      let lines: Vec<&str> = value.split('\n').collect();

      let min_indent = lines
        .iter()
        .skip(1)
        .filter(|line| !line.trim().is_empty())
        .map(|line| line.len() - line.trim_start().len())
        .min()
        .unwrap_or(0);

      let indented: Vec<String> = lines
        .iter()
        .enumerate()
        .map(|(index, line)| {
          if index == 0 {
            line.to_string()
          } else if line.trim().is_empty() {
            String::new()
          } else {
            let relative = &line[min_indent..];
            format!("{}{}", item_indent, relative)
          }
        })
        .collect();

      format!("- {}", indented.join("\n"))
    } else {
      format!("- {}", value)
    };

    match position {
      InsertPosition::Last => {
        let last_entry = entries.last().unwrap();
        let new_text = format!("\n{}{}", indent, new_item);

        self.insert_after_node(last_entry.syntax(), &new_text)
      }

      InsertPosition::At(index) => {
        if index >= entries.len() {
          let last_entry = entries.last().unwrap();
          let new_text = format!("\n{}{}", indent, new_item);

          self.insert_after_node(last_entry.syntax(), &new_text)
        } else {
          let target_entry = &entries[index];
          let target_range = target_entry.syntax().text_range();
          let replacement = format!("{}\n{}", new_item, indent);
          let insert_range = TextRange::new(target_range.start(), target_range.start());

          self.apply_edit(insert_range, &replacement)
        }
      }

      InsertPosition::Before(target_value) => {
        let target_entry = entries
          .iter()
          .find(|entry| {
            entry
              .flow()
              .and_then(|flow| extract_scalar_text(flow.syntax()))
              .map(|text| text == target_value)
              .unwrap_or(false)
          })
          .ok_or_else(|| YerbaError::SelectorNotFound(format!("{} item '{}'", dot_path, target_value)))?;

        let target_range = target_entry.syntax().text_range();
        let replacement = format!("{}\n{}", new_item, indent);
        let insert_range = TextRange::new(target_range.start(), target_range.start());

        self.apply_edit(insert_range, &replacement)
      }

      InsertPosition::After(target_value) => {
        let target_entry = entries
          .iter()
          .find(|entry| {
            entry
              .flow()
              .and_then(|flow| extract_scalar_text(flow.syntax()))
              .map(|text| text == target_value)
              .unwrap_or(false)
          })
          .ok_or_else(|| YerbaError::SelectorNotFound(format!("{} item '{}'", dot_path, target_value)))?;

        let new_text = format!("\n{}{}", indent, new_item);

        self.insert_after_node(target_entry.syntax(), &new_text)
      }

      InsertPosition::BeforeCondition(condition) => {
        let target_entry = entries
          .iter()
          .find(|entry| self.evaluate_condition_on_node(entry.syntax(), &condition))
          .ok_or_else(|| YerbaError::SelectorNotFound(format!("{} condition '{}'", dot_path, condition)))?;

        let target_range = target_entry.syntax().text_range();
        let replacement = format!("{}\n{}", new_item, indent);
        let insert_range = TextRange::new(target_range.start(), target_range.start());

        self.apply_edit(insert_range, &replacement)
      }

      InsertPosition::AfterCondition(condition) => {
        let target_entry = entries
          .iter()
          .find(|entry| self.evaluate_condition_on_node(entry.syntax(), &condition))
          .ok_or_else(|| YerbaError::SelectorNotFound(format!("{} condition '{}'", dot_path, condition)))?;

        let new_text = format!("\n{}{}", indent, new_item);

        self.insert_after_node(target_entry.syntax(), &new_text)
      }

      InsertPosition::FromSortOrder(_) => {
        let last_entry = entries.last().unwrap();
        let new_text = format!("\n{}{}", indent, new_item);

        self.insert_after_node(last_entry.syntax(), &new_text)
      }
    }
  }

  fn insert_map_key(&mut self, dot_path: &str, key: &str, value: &str, position: InsertPosition) -> Result<(), YerbaError> {
    let current_node = self.navigate(dot_path)?;

    let map = current_node
      .descendants()
      .find_map(BlockMap::cast)
      .ok_or_else(|| YerbaError::SelectorNotFound(dot_path.to_string()))?;

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

    if entries.is_empty() {
      let indent = preceding_whitespace_indent(map.syntax());
      let new_entry = format!("\n{}{}: {}", indent, key, value);

      return self.insert_after_node(map.syntax(), &new_entry);
    }

    if find_entry_by_key(&map, key).is_some() {
      return Err(YerbaError::ParseError(format!("key '{}' already exists at '{}'", key, dot_path)));
    }

    let indent = entries
      .get(1)
      .or(entries.first())
      .map(|entry| preceding_whitespace_indent(entry.syntax()))
      .unwrap_or_default();

    let new_entry_text = format!("{}: {}", key, value);

    match position {
      InsertPosition::Last => {
        let last_entry = entries.last().unwrap();
        let new_text = format!("\n{}{}", indent, new_entry_text);

        self.insert_after_node(last_entry.syntax(), &new_text)
      }

      InsertPosition::At(index) => {
        if index >= entries.len() {
          let last_entry = entries.last().unwrap();
          let new_text = format!("\n{}{}", indent, new_entry_text);

          self.insert_after_node(last_entry.syntax(), &new_text)
        } else {
          let target_entry = &entries[index];
          let target_range = target_entry.syntax().text_range();
          let replacement = format!("{}\n{}", new_entry_text, indent);
          let insert_range = TextRange::new(target_range.start(), target_range.start());

          self.apply_edit(insert_range, &replacement)
        }
      }

      InsertPosition::Before(target_key) => {
        let target_entry = find_entry_by_key(&map, &target_key).ok_or_else(|| YerbaError::SelectorNotFound(format!("{}.{}", dot_path, target_key)))?;

        let target_range = target_entry.syntax().text_range();
        let replacement = format!("{}\n{}", new_entry_text, indent);
        let insert_range = TextRange::new(target_range.start(), target_range.start());

        self.apply_edit(insert_range, &replacement)
      }

      InsertPosition::After(target_key) => {
        let target_entry = find_entry_by_key(&map, &target_key).ok_or_else(|| YerbaError::SelectorNotFound(format!("{}.{}", dot_path, target_key)))?;

        let new_text = format!("\n{}{}", indent, new_entry_text);

        self.insert_after_node(target_entry.syntax(), &new_text)
      }

      InsertPosition::BeforeCondition(_) | InsertPosition::AfterCondition(_) => self.insert_map_key(dot_path, key, value, InsertPosition::Last),

      InsertPosition::FromSortOrder(order) => {
        let new_key_position = order.iter().position(|ordered_key| ordered_key == key);

        let resolved = match new_key_position {
          Some(new_position) => {
            let mut insert_after: Option<String> = None;

            for ordered_key in order.iter().take(new_position).rev() {
              if find_entry_by_key(&map, ordered_key).is_some() {
                insert_after = Some(ordered_key.clone());
                break;
              }
            }

            match insert_after {
              Some(after_key) => InsertPosition::After(after_key),
              None => InsertPosition::At(0),
            }
          }

          None => InsertPosition::Last,
        };

        self.insert_map_key(dot_path, key, value, resolved)
      }
    }
  }
}