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