Skip to main content

braze_sync/resource/
tag.rs

1//! Tags are managed in **registry mode**, derived from references on
2//! other resources.
3//!
4//! Braze does not expose a public REST API for workspace tags — they
5//! cannot be listed, created, updated, or deleted programmatically. Tags
6//! exist only as embedded fields on other resources (content blocks,
7//! email templates, campaigns, canvases). Workspace administrators
8//! create tags through the Braze dashboard.
9//!
10//! braze-sync therefore tracks tags as a registry whose entries are
11//! sourced from references on managed resources. The registry is the
12//! GitOps contract: any tag a resource references must appear here, and
13//! `apply` fails fast (before mutating any resource) when a referenced
14//! tag is missing — instead of letting Braze return
15//! `400 "The following Tags could not be found: [...]"` mid-pipeline.
16
17use serde::{Deserialize, Serialize};
18
19#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
20pub struct TagRegistry {
21    pub tags: Vec<Tag>,
22}
23
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
25pub struct Tag {
26    pub name: String,
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub description: Option<String>,
29}
30
31impl TagRegistry {
32    pub fn normalized(&self) -> Self {
33        let mut sorted = self.clone();
34        sorted.tags.sort_by(|a, b| a.name.cmp(&b.name));
35        sorted
36    }
37
38    pub fn contains(&self, name: &str) -> bool {
39        self.tags.iter().any(|t| t.name == name)
40    }
41}
42
43#[cfg(test)]
44mod tests {
45    use super::*;
46
47    #[test]
48    fn registry_yaml_roundtrip() {
49        let r = TagRegistry {
50            tags: vec![
51                Tag {
52                    name: "campaign".into(),
53                    description: None,
54                },
55                Tag {
56                    name: "ad_slot/dialog".into(),
57                    description: Some("Dialog ad slot".into()),
58                },
59            ],
60        };
61        let yaml = serde_norway::to_string(&r).unwrap();
62        let parsed: TagRegistry = serde_norway::from_str(&yaml).unwrap();
63        assert_eq!(r, parsed);
64    }
65
66    #[test]
67    fn normalized_sorts_tags_by_name() {
68        let r = TagRegistry {
69            tags: vec![
70                Tag {
71                    name: "z".into(),
72                    description: None,
73                },
74                Tag {
75                    name: "a".into(),
76                    description: None,
77                },
78            ],
79        };
80        let n = r.normalized();
81        assert_eq!(n.tags[0].name, "a");
82        assert_eq!(n.tags[1].name, "z");
83    }
84
85    #[test]
86    fn description_none_is_omitted_in_output() {
87        let r = TagRegistry {
88            tags: vec![Tag {
89                name: "x".into(),
90                description: None,
91            }],
92        };
93        let yaml = serde_norway::to_string(&r).unwrap();
94        assert!(!yaml.contains("description"));
95    }
96
97    #[test]
98    fn contains_finds_existing_name() {
99        let r = TagRegistry {
100            tags: vec![Tag {
101                name: "promo".into(),
102                description: None,
103            }],
104        };
105        assert!(r.contains("promo"));
106        assert!(!r.contains("missing"));
107    }
108}