use crate::error::{Error, Result};
use crate::resource::CustomAttributeRegistry;
use std::path::Path;
use super::write_atomic;
const REGISTRY_HEADER: &str = "# Generated by braze-sync.\n\
#\n\
# This is a REGISTRY of Custom Attributes that exist in Braze.\n\
# braze-sync cannot create new Custom Attributes — Braze creates them\n\
# implicitly when /users/track receives data containing them.\n\
# braze-sync can only mark them as deprecated.\n";
pub fn load_registry(path: &Path) -> Result<Option<CustomAttributeRegistry>> {
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: CustomAttributeRegistry =
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: &CustomAttributeRegistry) -> 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::{CustomAttribute, CustomAttributeRegistry, CustomAttributeType};
fn sample_registry() -> CustomAttributeRegistry {
CustomAttributeRegistry {
attributes: vec![
CustomAttribute {
name: "preferred_clinic_id".into(),
attribute_type: CustomAttributeType::String,
description: Some("User's preferred clinic".into()),
deprecated: false,
},
CustomAttribute {
name: "last_visit_date".into(),
attribute_type: CustomAttributeType::Time,
description: Some("Most recent visit timestamp".into()),
deprecated: false,
},
CustomAttribute {
name: "legacy_segment".into(),
attribute_type: CustomAttributeType::String,
description: None,
deprecated: true,
},
],
}
}
#[test]
fn save_load_roundtrip() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("registry.yaml");
let registry = sample_registry();
save_registry(&path, ®istry).unwrap();
let loaded = load_registry(&path).unwrap().unwrap();
let normalized = registry.normalized();
assert_eq!(loaded, normalized);
}
#[test]
fn save_normalizes_order() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("registry.yaml");
let registry = sample_registry();
save_registry(&path, ®istry).unwrap();
let content = std::fs::read_to_string(&path).unwrap();
let pos_last = content.find("last_visit_date").unwrap();
let pos_legacy = content.find("legacy_segment").unwrap();
let pos_pref = content.find("preferred_clinic_id").unwrap();
assert!(pos_last < pos_legacy);
assert!(pos_legacy < pos_pref);
}
#[test]
fn save_includes_header_comment() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("registry.yaml");
save_registry(&path, &sample_registry()).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");
let result = load_registry(&path).unwrap();
assert!(result.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 deprecated_false_is_omitted_in_output() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("registry.yaml");
let registry = CustomAttributeRegistry {
attributes: vec![CustomAttribute {
name: "active_attr".into(),
attribute_type: CustomAttributeType::String,
description: None,
deprecated: false,
}],
};
save_registry(&path, ®istry).unwrap();
let content = std::fs::read_to_string(&path).unwrap();
assert!(!content.contains("deprecated:"));
}
#[test]
fn deprecated_true_is_preserved() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("registry.yaml");
let registry = CustomAttributeRegistry {
attributes: vec![CustomAttribute {
name: "old_attr".into(),
attribute_type: CustomAttributeType::String,
description: None,
deprecated: true,
}],
};
save_registry(&path, ®istry).unwrap();
let content = std::fs::read_to_string(&path).unwrap();
assert!(content.contains("deprecated: true"));
}
#[test]
fn empty_registry_roundtrips() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("registry.yaml");
let registry = CustomAttributeRegistry { attributes: vec![] };
save_registry(&path, ®istry).unwrap();
let loaded = load_registry(&path).unwrap().unwrap();
assert!(loaded.attributes.is_empty());
}
}