Skip to main content

braze_sync/config/
schema.rs

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