Skip to main content

braze_sync/config/
schema.rs

1//! Raw configuration types deserialized from `braze-sync.config.yaml`.
2//!
3//! Every struct here uses `#[serde(deny_unknown_fields)]` — the config file is
4//! the **only** place in braze-sync where unknown fields are rejected. Resource
5//! files (`schema.yaml`, `template.yaml`, etc.) stay forward-compat permissive.
6
7use serde::Deserialize;
8use std::collections::BTreeMap;
9use std::path::PathBuf;
10use url::Url;
11
12#[derive(Debug, Clone, Deserialize)]
13#[serde(deny_unknown_fields)]
14pub struct ConfigFile {
15    /// Schema version. v1.0 binaries accept exactly `1`. Bumping this is a
16    /// breaking event by design.
17    pub version: u32,
18    pub default_environment: String,
19    #[serde(default)]
20    pub defaults: Defaults,
21    pub environments: BTreeMap<String, EnvironmentConfig>,
22    #[serde(default)]
23    pub resources: ResourcesConfig,
24    #[serde(default)]
25    pub naming: NamingConfig,
26}
27
28#[derive(Debug, Clone, Default, Deserialize)]
29#[serde(deny_unknown_fields)]
30pub struct Defaults {}
31
32#[derive(Debug, Clone, Deserialize)]
33pub struct EnvironmentConfig {
34    pub api_endpoint: Url,
35    pub api_key_env: String,
36}
37
38#[derive(Debug, Clone, Deserialize)]
39#[serde(deny_unknown_fields)]
40pub struct ResourcesConfig {
41    #[serde(default = "default_catalog_schema")]
42    pub catalog_schema: ResourceConfig,
43    #[serde(default = "default_content_block")]
44    pub content_block: ResourceConfig,
45    #[serde(default = "default_email_template")]
46    pub email_template: ResourceConfig,
47    #[serde(default = "default_custom_attribute")]
48    pub custom_attribute: ResourceConfig,
49    #[serde(default = "default_tag")]
50    pub tag: ResourceConfig,
51}
52
53impl ResourcesConfig {
54    pub fn for_kind(&self, kind: crate::resource::ResourceKind) -> &ResourceConfig {
55        use crate::resource::ResourceKind;
56        match kind {
57            ResourceKind::CatalogSchema => &self.catalog_schema,
58            ResourceKind::ContentBlock => &self.content_block,
59            ResourceKind::EmailTemplate => &self.email_template,
60            ResourceKind::CustomAttribute => &self.custom_attribute,
61            ResourceKind::Tag => &self.tag,
62        }
63    }
64
65    pub fn is_enabled(&self, kind: crate::resource::ResourceKind) -> bool {
66        self.for_kind(kind).enabled
67    }
68}
69
70impl Default for ResourcesConfig {
71    fn default() -> Self {
72        Self {
73            catalog_schema: default_catalog_schema(),
74            content_block: default_content_block(),
75            email_template: default_email_template(),
76            custom_attribute: default_custom_attribute(),
77            tag: default_tag(),
78        }
79    }
80}
81
82#[derive(Debug, Clone, Deserialize)]
83#[serde(deny_unknown_fields)]
84pub struct ResourceConfig {
85    #[serde(default = "default_enabled")]
86    pub enabled: bool,
87    pub path: PathBuf,
88    /// Regex patterns (matched against resource `name`) that mark a
89    /// resource as **managed out of band**. Names matching any pattern
90    /// are skipped by `export`, `diff`, `apply`, and `validate` so
91    /// Braze reserved attributes (`_unset`) or camelCase duplicates
92    /// don't produce noise. See `docs/configuration.md §exclude_patterns`.
93    #[serde(default)]
94    pub exclude_patterns: Vec<String>,
95    /// Apply-time ordering policy. Currently consulted only by
96    /// `content_block` apply, which uses `Dependency` to topologically
97    /// sort `{{content_blocks.${other}}}` references so a referrer is
98    /// never created before its target. The field is shared on
99    /// `ResourceConfig` (rather than scoped to a content_block-only
100    /// type) to keep `ResourcesConfig::for_kind` type-stable; setting
101    /// it on other resource kinds is accepted but inert.
102    #[serde(default)]
103    pub apply_order: ApplyOrder,
104}
105
106/// Apply-time ordering policy. `Dependency` topo-sorts content_blocks
107/// so a referrer is never created before its target (see
108/// `diff::content_block_order`). `Alphabetical` skips that pass and
109/// applies in name order — kept for callers who built tooling around
110/// the exact apply sequence.
111#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
112#[serde(rename_all = "lowercase")]
113pub enum ApplyOrder {
114    #[default]
115    Dependency,
116    Alphabetical,
117}
118
119fn default_enabled() -> bool {
120    true
121}
122
123fn default_catalog_schema() -> ResourceConfig {
124    ResourceConfig {
125        enabled: true,
126        path: PathBuf::from("catalogs/"),
127        exclude_patterns: Vec::new(),
128        apply_order: ApplyOrder::Dependency,
129    }
130}
131
132fn default_content_block() -> ResourceConfig {
133    ResourceConfig {
134        enabled: true,
135        path: PathBuf::from("content_blocks/"),
136        exclude_patterns: Vec::new(),
137        apply_order: ApplyOrder::Dependency,
138    }
139}
140
141fn default_email_template() -> ResourceConfig {
142    ResourceConfig {
143        enabled: true,
144        path: PathBuf::from("email_templates/"),
145        exclude_patterns: Vec::new(),
146        apply_order: ApplyOrder::Dependency,
147    }
148}
149
150fn default_custom_attribute() -> ResourceConfig {
151    ResourceConfig {
152        enabled: true,
153        path: PathBuf::from("custom_attributes/registry.yaml"),
154        exclude_patterns: Vec::new(),
155        apply_order: ApplyOrder::Dependency,
156    }
157}
158
159fn default_tag() -> ResourceConfig {
160    // Opt-in: enabling without a registry file would flag every tag
161    // reference in existing resources as undeclared on first validate.
162    ResourceConfig {
163        enabled: false,
164        path: PathBuf::from("tags/registry.yaml"),
165        exclude_patterns: Vec::new(),
166        apply_order: ApplyOrder::Dependency,
167    }
168}
169
170#[derive(Debug, Clone, Default, Deserialize)]
171#[serde(deny_unknown_fields)]
172pub struct NamingConfig {
173    #[serde(default)]
174    pub catalog_name_pattern: Option<String>,
175    #[serde(default)]
176    pub content_block_name_pattern: Option<String>,
177    #[serde(default)]
178    pub custom_attribute_name_pattern: Option<String>,
179    #[serde(default)]
180    pub tag_name_pattern: Option<String>,
181}