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    /// Optional override for the per-env values file location. When unset
42    /// the CLI resolves `values/<env>.yaml` relative to the config dir
43    /// (per RFC `feat-per-env-values.md` §2.1). The file itself is also
44    /// optional — a missing file is OK as long as no resource body
45    /// references a `__BRAZESYNC.…__` placeholder.
46    #[serde(default)]
47    pub values_file: Option<PathBuf>,
48}
49
50#[derive(Debug, Clone, Deserialize)]
51#[serde(deny_unknown_fields)]
52pub struct ResourcesConfig {
53    #[serde(default = "default_catalog_schema")]
54    pub catalog_schema: ResourceConfig,
55    #[serde(default = "default_content_block")]
56    pub content_block: ResourceConfig,
57    #[serde(default = "default_email_template")]
58    pub email_template: ResourceConfig,
59    #[serde(default = "default_custom_attribute")]
60    pub custom_attribute: ResourceConfig,
61    #[serde(default = "default_tag")]
62    pub tag: ResourceConfig,
63}
64
65impl ResourcesConfig {
66    pub fn for_kind(&self, kind: crate::resource::ResourceKind) -> &ResourceConfig {
67        use crate::resource::ResourceKind;
68        match kind {
69            ResourceKind::CatalogSchema => &self.catalog_schema,
70            ResourceKind::ContentBlock => &self.content_block,
71            ResourceKind::EmailTemplate => &self.email_template,
72            ResourceKind::CustomAttribute => &self.custom_attribute,
73            ResourceKind::Tag => &self.tag,
74        }
75    }
76
77    pub fn is_enabled(&self, kind: crate::resource::ResourceKind) -> bool {
78        self.for_kind(kind).enabled
79    }
80}
81
82impl Default for ResourcesConfig {
83    fn default() -> Self {
84        Self {
85            catalog_schema: default_catalog_schema(),
86            content_block: default_content_block(),
87            email_template: default_email_template(),
88            custom_attribute: default_custom_attribute(),
89            tag: default_tag(),
90        }
91    }
92}
93
94#[derive(Debug, Clone, Deserialize)]
95#[serde(deny_unknown_fields)]
96pub struct ResourceConfig {
97    #[serde(default = "default_enabled")]
98    pub enabled: bool,
99    pub path: PathBuf,
100    /// Regex patterns (matched against resource `name`) that mark a
101    /// resource as **managed out of band**. Names matching any pattern
102    /// are skipped by `export`, `diff`, `apply`, and `validate` so
103    /// Braze reserved attributes (`_unset`) or camelCase duplicates
104    /// don't produce noise. See `docs/configuration.md §exclude_patterns`.
105    #[serde(default)]
106    pub exclude_patterns: Vec<String>,
107    /// Apply-time ordering policy. Currently consulted only by
108    /// `content_block` apply, which uses `Dependency` to topologically
109    /// sort `{{content_blocks.${other}}}` references so a referrer is
110    /// never created before its target. The field is shared on
111    /// `ResourceConfig` (rather than scoped to a content_block-only
112    /// type) to keep `ResourcesConfig::for_kind` type-stable; setting
113    /// it on other resource kinds is accepted but inert.
114    #[serde(default)]
115    pub apply_order: ApplyOrder,
116}
117
118/// Apply-time ordering policy. `Dependency` topo-sorts content_blocks
119/// so a referrer is never created before its target (see
120/// `diff::content_block_order`). `Alphabetical` skips that pass and
121/// applies in name order — kept for callers who built tooling around
122/// the exact apply sequence.
123#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
124#[serde(rename_all = "lowercase")]
125pub enum ApplyOrder {
126    #[default]
127    Dependency,
128    Alphabetical,
129}
130
131fn default_enabled() -> bool {
132    true
133}
134
135fn default_catalog_schema() -> ResourceConfig {
136    ResourceConfig {
137        enabled: true,
138        path: PathBuf::from("catalogs/"),
139        exclude_patterns: Vec::new(),
140        apply_order: ApplyOrder::Dependency,
141    }
142}
143
144fn default_content_block() -> ResourceConfig {
145    ResourceConfig {
146        enabled: true,
147        path: PathBuf::from("content_blocks/"),
148        exclude_patterns: Vec::new(),
149        apply_order: ApplyOrder::Dependency,
150    }
151}
152
153fn default_email_template() -> ResourceConfig {
154    ResourceConfig {
155        enabled: true,
156        path: PathBuf::from("email_templates/"),
157        exclude_patterns: Vec::new(),
158        apply_order: ApplyOrder::Dependency,
159    }
160}
161
162fn default_custom_attribute() -> ResourceConfig {
163    ResourceConfig {
164        enabled: true,
165        path: PathBuf::from("custom_attributes/registry.yaml"),
166        exclude_patterns: Vec::new(),
167        apply_order: ApplyOrder::Dependency,
168    }
169}
170
171fn default_tag() -> ResourceConfig {
172    // Opt-in: enabling without a registry file would flag every tag
173    // reference in existing resources as undeclared on first validate.
174    ResourceConfig {
175        enabled: false,
176        path: PathBuf::from("tags/registry.yaml"),
177        exclude_patterns: Vec::new(),
178        apply_order: ApplyOrder::Dependency,
179    }
180}
181
182#[derive(Debug, Clone, Default, Deserialize)]
183#[serde(deny_unknown_fields)]
184pub struct NamingConfig {
185    #[serde(default)]
186    pub catalog_name_pattern: Option<String>,
187    #[serde(default)]
188    pub content_block_name_pattern: Option<String>,
189    #[serde(default)]
190    pub custom_attribute_name_pattern: Option<String>,
191    #[serde(default)]
192    pub tag_name_pattern: Option<String>,
193}