use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use gradatum_core::frontmatter::Frontmatter;
use gradatum_core::overrides::{FrontmatterPatch, Overridable, OverrideMeta, OverridePayload};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NoteMetadataOverride {
pub meta: OverrideMeta,
pub patch: FrontmatterPatch,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub extra_overlay: BTreeMap<String, toml::Value>,
}
impl OverridePayload for NoteMetadataOverride {
const OVERRIDE_TYPE: &'static str = "metadata";
const SCHEMA_VERSION: gradatum_core::frontmatter::SchemaVersion = 1;
}
impl Overridable for NoteMetadataOverride {
type Patch = FrontmatterPatch;
type Output = Frontmatter;
fn resolve(base: &Frontmatter, patch: &FrontmatterPatch) -> Frontmatter {
let mut effective = base.clone();
if let Some(section) = patch.section {
effective.section = section;
}
for tag in &patch.tags_add {
if !effective.tags.contains(tag) {
effective.tags.push(tag.clone());
}
}
effective.tags.retain(|t| !patch.tags_remove.contains(t));
if let Some(status) = patch.status {
effective.status = status;
}
if let Some(ref author) = patch.author_override {
effective.author = Some(author.clone());
}
if patch.status_reason.is_some() {
effective.status_reason = patch.status_reason.clone();
}
effective
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
use gradatum_core::scope::VaultId;
use gradatum_core::section::Section;
use gradatum_core::status::NoteStatus;
use gradatum_core::tag::Tag;
fn build_frontmatter(status: NoteStatus) -> Frontmatter {
Frontmatter {
schema_version: 1,
vault_id: VaultId::new("main"),
locus: None,
section: Section::Decisions,
status,
status_reason: None,
status_changed: None,
tags: Default::default(),
author: None,
created: Utc::now(),
updated: None,
extra: Default::default(),
provenance: None,
forgotten: None,
forgotten_at: None,
forgotten_by: None,
}
}
#[test]
fn resolve_applies_status_change() {
let base = build_frontmatter(NoteStatus::Draft);
let patch = FrontmatterPatch {
status: Some(NoteStatus::Live),
..Default::default()
};
let effective = NoteMetadataOverride::resolve(&base, &patch);
assert_eq!(effective.status, NoteStatus::Live);
assert_eq!(effective.section, Section::Decisions);
}
#[test]
fn resolve_appends_tags_no_duplicate() {
let mut base = build_frontmatter(NoteStatus::Draft);
base.tags.push(Tag::new("rust").unwrap());
let patch = FrontmatterPatch {
tags_add: vec![Tag::new("rust").unwrap(), Tag::new("gradatum").unwrap()],
..Default::default()
};
let effective = NoteMetadataOverride::resolve(&base, &patch);
assert_eq!(effective.tags.len(), 2);
assert!(effective.tags.contains(&Tag::new("rust").unwrap()));
assert!(effective.tags.contains(&Tag::new("gradatum").unwrap()));
}
#[test]
fn resolve_removes_tags() {
let mut base = build_frontmatter(NoteStatus::Draft);
base.tags.push(Tag::new("old-tag").unwrap());
base.tags.push(Tag::new("keep-tag").unwrap());
let patch = FrontmatterPatch {
tags_remove: vec![Tag::new("old-tag").unwrap()],
..Default::default()
};
let effective = NoteMetadataOverride::resolve(&base, &patch);
assert_eq!(effective.tags.len(), 1);
assert!(effective.tags.contains(&Tag::new("keep-tag").unwrap()));
assert!(!effective.tags.contains(&Tag::new("old-tag").unwrap()));
}
#[test]
fn resolve_empty_patch_is_identity() {
let base = build_frontmatter(NoteStatus::Live);
let patch = FrontmatterPatch::default();
let effective = NoteMetadataOverride::resolve(&base, &patch);
assert_eq!(effective.status, base.status);
assert_eq!(effective.section, base.section);
assert_eq!(effective.tags, base.tags);
}
}