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, skip_serializing_if = "std::ops::Not::not")]
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 CustomAttributeType {
45    /// The lowercase wire string for this attribute type ("string",
46    /// "number", ...). Matches the snake_case `Serialize` representation
47    /// derived above so the wire string and the explicit method cannot
48    /// drift.
49    pub fn as_str(self) -> &'static str {
50        match self {
51            Self::String => "string",
52            Self::Number => "number",
53            Self::Boolean => "boolean",
54            Self::Time => "time",
55            Self::Array => "array",
56        }
57    }
58}
59
60impl CustomAttributeRegistry {
61    pub fn normalized(&self) -> Self {
62        let mut sorted = self.clone();
63        sorted.attributes.sort_by(|a, b| a.name.cmp(&b.name));
64        sorted
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn registry_yaml_roundtrip() {
74        let r = CustomAttributeRegistry {
75            attributes: vec![
76                CustomAttribute {
77                    name: "last_visit".into(),
78                    attribute_type: CustomAttributeType::Time,
79                    description: Some("Most recent visit".into()),
80                    deprecated: false,
81                },
82                CustomAttribute {
83                    name: "legacy_segment".into(),
84                    attribute_type: CustomAttributeType::String,
85                    description: None,
86                    deprecated: true,
87                },
88            ],
89        };
90        let yaml = serde_norway::to_string(&r).unwrap();
91        let parsed: CustomAttributeRegistry = serde_norway::from_str(&yaml).unwrap();
92        assert_eq!(r, parsed);
93    }
94
95    #[test]
96    fn deprecated_defaults_to_false() {
97        let yaml = "name: foo\ntype: string\n";
98        let attr: CustomAttribute = serde_norway::from_str(yaml).unwrap();
99        assert!(!attr.deprecated);
100    }
101
102    #[test]
103    fn normalized_sorts_attributes_by_name() {
104        let r = CustomAttributeRegistry {
105            attributes: vec![
106                CustomAttribute {
107                    name: "z".into(),
108                    attribute_type: CustomAttributeType::String,
109                    description: None,
110                    deprecated: false,
111                },
112                CustomAttribute {
113                    name: "a".into(),
114                    attribute_type: CustomAttributeType::String,
115                    description: None,
116                    deprecated: false,
117                },
118            ],
119        };
120        let n = r.normalized();
121        assert_eq!(n.attributes[0].name, "a");
122        assert_eq!(n.attributes[1].name, "z");
123    }
124}