#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MergeStrategy {
Replace,
Append,
Prepend,
Section(String),
}
impl MergeStrategy {
pub fn parse(s: &str) -> Self {
let trimmed = s.trim();
if let Some(rest) = trimmed.strip_prefix("section:") {
return Self::Section(rest.to_string());
}
match trimmed.to_ascii_lowercase().as_str() {
"append" => Self::Append,
"prepend" => Self::Prepend,
"replace" | "" => Self::Replace,
_ => Self::Replace,
}
}
pub fn merge(&self, base: &str, overlay: &str) -> String {
match self {
Self::Replace => overlay.to_string(),
Self::Append => {
if base.is_empty() {
overlay.to_string()
} else if base.ends_with('\n') {
format!("{base}{overlay}")
} else {
format!("{base}\n{overlay}")
}
}
Self::Prepend => {
if base.is_empty() {
overlay.to_string()
} else if overlay.ends_with('\n') {
format!("{overlay}{base}")
} else {
format!("{overlay}\n{base}")
}
}
Self::Section(name) => {
let marker = format!("{{{{!-- {name} --}}}}");
if base.contains(&marker) {
base.replace(&marker, overlay)
} else {
Self::Append.merge(base, overlay)
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_default_is_replace() {
assert_eq!(MergeStrategy::parse(""), MergeStrategy::Replace);
assert_eq!(MergeStrategy::parse("replace"), MergeStrategy::Replace);
assert_eq!(MergeStrategy::parse("REPLACE"), MergeStrategy::Replace);
assert_eq!(MergeStrategy::parse("nonsense"), MergeStrategy::Replace);
}
#[test]
fn parse_append_prepend() {
assert_eq!(MergeStrategy::parse("append"), MergeStrategy::Append);
assert_eq!(MergeStrategy::parse(" Append "), MergeStrategy::Append);
assert_eq!(MergeStrategy::parse("prepend"), MergeStrategy::Prepend);
}
#[test]
fn parse_section_with_name() {
match MergeStrategy::parse("section:emote") {
MergeStrategy::Section(name) => assert_eq!(name, "emote"),
_ => panic!("expected Section"),
}
}
#[test]
fn replace_overrides_base() {
let out = MergeStrategy::Replace.merge("base body", "overlay body");
assert_eq!(out, "overlay body");
}
#[test]
fn append_concatenates_with_newline() {
let out = MergeStrategy::Append.merge("## Active set\n- foo", "(^_^)");
assert_eq!(out, "## Active set\n- foo\n(^_^)");
}
#[test]
fn append_preserves_trailing_newline() {
let out = MergeStrategy::Append.merge("base\n", "tail");
assert_eq!(out, "base\ntail");
}
#[test]
fn prepend_puts_overlay_first() {
let out = MergeStrategy::Prepend.merge("base body", "## emote-header");
assert_eq!(out, "## emote-header\nbase body");
}
#[test]
fn section_replaces_marker() {
let base = "## Active set\n{{!-- emote --}}\n- list";
let out = MergeStrategy::Section("emote".to_string()).merge(base, "(^_^)");
assert_eq!(out, "## Active set\n(^_^)\n- list");
}
#[test]
fn section_falls_back_to_append_when_marker_missing() {
let base = "## Active set\n- list";
let out = MergeStrategy::Section("nonexistent".to_string()).merge(base, "(^_^)");
assert_eq!(out, "## Active set\n- list\n(^_^)");
}
#[test]
fn empty_base_returns_overlay_for_append_prepend() {
assert_eq!(MergeStrategy::Append.merge("", "ov"), "ov");
assert_eq!(MergeStrategy::Prepend.merge("", "ov"), "ov");
}
}