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}
101
102fn default_enabled() -> bool {
103    true
104}
105
106fn default_catalog_schema() -> ResourceConfig {
107    ResourceConfig {
108        enabled: true,
109        path: PathBuf::from("catalogs/"),
110        exclude_patterns: Vec::new(),
111    }
112}
113
114fn default_content_block() -> ResourceConfig {
115    ResourceConfig {
116        enabled: true,
117        path: PathBuf::from("content_blocks/"),
118        exclude_patterns: Vec::new(),
119    }
120}
121
122fn default_email_template() -> ResourceConfig {
123    ResourceConfig {
124        enabled: true,
125        path: PathBuf::from("email_templates/"),
126        exclude_patterns: Vec::new(),
127    }
128}
129
130fn default_custom_attribute() -> ResourceConfig {
131    ResourceConfig {
132        enabled: true,
133        path: PathBuf::from("custom_attributes/registry.yaml"),
134        exclude_patterns: Vec::new(),
135    }
136}
137
138fn default_tag() -> ResourceConfig {
139    // Opt-in: enabling without a registry file would flag every tag
140    // reference in existing resources as undeclared on first validate.
141    ResourceConfig {
142        enabled: false,
143        path: PathBuf::from("tags/registry.yaml"),
144        exclude_patterns: Vec::new(),
145    }
146}
147
148#[derive(Debug, Clone, Default, Deserialize)]
149#[serde(deny_unknown_fields)]
150pub struct NamingConfig {
151    #[serde(default)]
152    pub catalog_name_pattern: Option<String>,
153    #[serde(default)]
154    pub content_block_name_pattern: Option<String>,
155    #[serde(default)]
156    pub custom_attribute_name_pattern: Option<String>,
157    #[serde(default)]
158    pub tag_name_pattern: Option<String>,
159}