use crate::error::{Error, Result};
use crate::resource::TagRegistry;
use std::path::Path;
use super::write_atomic;
const REGISTRY_HEADER: &str = "# Generated by braze-sync.\n\
#\n\
# This is a REGISTRY of Tags expected to exist in the target Braze\n\
# workspace. Braze does not expose a public REST API for managing\n\
# tags — they must be created in the Braze dashboard. This file\n\
# makes the dependency explicit so apply can fail fast (before any\n\
# mutation) when a referenced tag is missing.\n";
pub fn load_registry(path: &Path) -> Result<Option<TagRegistry>> {
let bytes = match std::fs::read_to_string(path) {
Ok(b) => b,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(e.into()),
};
let registry: TagRegistry =
serde_norway::from_str(&bytes).map_err(|source| Error::YamlParse {
path: path.to_path_buf(),
source,
})?;
Ok(Some(registry))
}
pub fn save_registry(path: &Path, registry: &TagRegistry) -> Result<()> {
let normalized = registry.normalized();
let yaml = serde_norway::to_string(&normalized).map_err(|e| Error::InvalidFormat {
path: path.to_path_buf(),
message: format!("yaml serialization failed: {e}"),
})?;
let mut out = String::with_capacity(REGISTRY_HEADER.len() + yaml.len());
out.push_str(REGISTRY_HEADER);
out.push_str(&yaml);
write_atomic(path, out.as_bytes())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::resource::{Tag, TagRegistry};
fn sample() -> TagRegistry {
TagRegistry {
tags: vec![
Tag {
name: "campaign".into(),
description: None,
},
Tag {
name: "ad_slot".into(),
description: Some("Ad slot tag".into()),
},
Tag {
name: "ad_slot/dialog".into(),
description: None,
},
],
}
}
#[test]
fn save_load_roundtrip() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("registry.yaml");
let r = sample();
save_registry(&path, &r).unwrap();
let loaded = load_registry(&path).unwrap().unwrap();
assert_eq!(loaded, r.normalized());
}
#[test]
fn save_normalizes_order() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("registry.yaml");
save_registry(&path, &sample()).unwrap();
let content = std::fs::read_to_string(&path).unwrap();
let pos_ad = content.find("name: ad_slot\n").unwrap();
let pos_dialog = content.find("ad_slot/dialog").unwrap();
let pos_campaign = content.find("campaign").unwrap();
assert!(pos_ad < pos_dialog);
assert!(pos_dialog < pos_campaign);
}
#[test]
fn save_includes_header_comment() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("registry.yaml");
save_registry(&path, &sample()).unwrap();
let content = std::fs::read_to_string(&path).unwrap();
assert!(content.starts_with("# Generated by braze-sync."));
assert!(content.contains("REGISTRY"));
}
#[test]
fn load_missing_file_returns_none() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("nonexistent.yaml");
assert!(load_registry(&path).unwrap().is_none());
}
#[test]
fn load_invalid_yaml_returns_error() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("registry.yaml");
std::fs::write(&path, "not: valid: yaml: [").unwrap();
let err = load_registry(&path).unwrap_err();
assert!(matches!(err, Error::YamlParse { .. }));
}
#[test]
fn empty_registry_roundtrips() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("registry.yaml");
let r = TagRegistry { tags: vec![] };
save_registry(&path, &r).unwrap();
let loaded = load_registry(&path).unwrap().unwrap();
assert!(loaded.tags.is_empty());
}
}