Skip to main content

braze_sync/resource/
custom_attribute.rs

1//! Custom Attributes are managed in **registry mode**.
2//!
3//! Braze creates Custom Attributes implicitly when `/users/track` receives
4//! data containing a previously-unseen attribute name. There is no
5//! declarative "create attribute" API. braze-sync therefore supports only:
6//!
7//! - `export`:   snapshot the current Braze attribute set into Git
8//! - `diff`:     show drift between local registry and Braze
9//! - `apply`:    toggle the deprecation flag — the *only* mutation
10//! - `validate`: structural check of the local YAML registry
11//!
12//! New attributes are introduced by application code via `/users/track`,
13//! never by braze-sync. See IMPLEMENTATION.md §2.2 / §6.5 / §11.5.
14
15use serde::{Deserialize, Serialize};
16
17#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
18pub struct CustomAttributeRegistry {
19    pub attributes: Vec<CustomAttribute>,
20}
21
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23pub struct CustomAttribute {
24    pub name: String,
25    #[serde(rename = "type")]
26    pub attribute_type: CustomAttributeType,
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub description: Option<String>,
29    /// Marks the attribute deprecated. The only mutation `apply` performs.
30    #[serde(default)]
31    pub deprecated: bool,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "snake_case")]
36pub enum CustomAttributeType {
37    String,
38    Number,
39    Boolean,
40    Time,
41    Array,
42}
43
44impl CustomAttributeRegistry {
45    pub fn normalized(&self) -> Self {
46        let mut sorted = self.clone();
47        sorted.attributes.sort_by(|a, b| a.name.cmp(&b.name));
48        sorted
49    }
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55
56    #[test]
57    fn registry_yaml_roundtrip() {
58        let r = CustomAttributeRegistry {
59            attributes: vec![
60                CustomAttribute {
61                    name: "last_visit".into(),
62                    attribute_type: CustomAttributeType::Time,
63                    description: Some("Most recent visit".into()),
64                    deprecated: false,
65                },
66                CustomAttribute {
67                    name: "legacy_segment".into(),
68                    attribute_type: CustomAttributeType::String,
69                    description: None,
70                    deprecated: true,
71                },
72            ],
73        };
74        let yaml = serde_norway::to_string(&r).unwrap();
75        let parsed: CustomAttributeRegistry = serde_norway::from_str(&yaml).unwrap();
76        assert_eq!(r, parsed);
77    }
78
79    #[test]
80    fn deprecated_defaults_to_false() {
81        let yaml = "name: foo\ntype: string\n";
82        let attr: CustomAttribute = serde_norway::from_str(yaml).unwrap();
83        assert!(!attr.deprecated);
84    }
85
86    #[test]
87    fn normalized_sorts_attributes_by_name() {
88        let r = CustomAttributeRegistry {
89            attributes: vec![
90                CustomAttribute {
91                    name: "z".into(),
92                    attribute_type: CustomAttributeType::String,
93                    description: None,
94                    deprecated: false,
95                },
96                CustomAttribute {
97                    name: "a".into(),
98                    attribute_type: CustomAttributeType::String,
99                    description: None,
100                    deprecated: false,
101                },
102            ],
103        };
104        let n = r.normalized();
105        assert_eq!(n.attributes[0].name, "a");
106        assert_eq!(n.attributes[1].name, "z");
107    }
108}