Skip to main content

braze_sync/cli/
templatize.rs

1//! `braze-sync templatize` — one-shot local rewrite of raw lid/cb_id
2//! literals to anonymous `__BRAZESYNC__` placeholders. No Braze API
3//! calls.
4
5use std::path::Path;
6
7use anyhow::Context as _;
8use clap::Args;
9
10use crate::config::ConfigFile;
11use crate::fs::{content_block_io, email_template_io};
12use crate::values::templatize::{templatize_body, FieldKind};
13
14#[derive(Args, Debug)]
15pub struct TemplatizeArgs {
16    /// Preview-only mode. Walks the same code path but does not touch
17    /// any file on disk. Prints a summary of what would change.
18    #[arg(long)]
19    pub dry_run: bool,
20}
21
22pub async fn run(args: &TemplatizeArgs, cfg: &ConfigFile, config_dir: &Path) -> anyhow::Result<()> {
23    let content_blocks_root = config_dir.join(&cfg.resources.content_block.path);
24    let email_templates_root = config_dir.join(&cfg.resources.email_template.path);
25
26    let mut summary = RunSummary::default();
27    let mut content_block_rewrites: Vec<(std::path::PathBuf, crate::resource::ContentBlock)> =
28        Vec::new();
29    let mut email_template_rewrites: Vec<crate::resource::EmailTemplate> = Vec::new();
30
31    if cfg.resources.content_block.enabled && content_blocks_root.exists() {
32        let blocks = content_block_io::load_all_content_blocks(&content_blocks_root)
33            .context("loading local content_blocks for templatize")?;
34        for mut cb in blocks {
35            let result = templatize_body(&cb.content, FieldKind::ContentBlock);
36            if result.lid_rewrites + result.cb_id_rewrites == 0 {
37                if cb.content.contains("__BRAZESYNC__") {
38                    summary.skipped.push(format!(
39                        "content_block '{}' already templated — skipping",
40                        cb.name
41                    ));
42                }
43                continue;
44            }
45            for w in &result.warnings {
46                summary
47                    .warnings
48                    .push(format!("content_block '{}': {w}", cb.name));
49            }
50            summary.touched_resources += 1;
51            summary.lid_rewrites += result.lid_rewrites;
52            summary.cb_id_rewrites += result.cb_id_rewrites;
53            cb.content = result.new_body;
54            let target = content_blocks_root.join(format!("{}.liquid", cb.name));
55            content_block_rewrites.push((target, cb));
56        }
57    }
58
59    if cfg.resources.email_template.enabled && email_templates_root.exists() {
60        let templates = email_template_io::load_all_email_templates(&email_templates_root)
61            .context("loading local email_templates for templatize")?;
62        for mut et in templates {
63            let already_templated = et.subject.contains("__BRAZESYNC__")
64                || et.body_html.contains("__BRAZESYNC__")
65                || et.body_plaintext.contains("__BRAZESYNC__")
66                || et
67                    .preheader
68                    .as_deref()
69                    .is_some_and(|p| p.contains("__BRAZESYNC__"));
70
71            let subject_r = templatize_body(&et.subject, FieldKind::EmailSubject);
72            let body_html_r = templatize_body(&et.body_html, FieldKind::EmailHtmlBody);
73            let body_plain_r = templatize_body(&et.body_plaintext, FieldKind::EmailPlainBody);
74            let preheader_r = et
75                .preheader
76                .as_ref()
77                .map(|p| templatize_body(p, FieldKind::EmailPreheader));
78
79            let total_rewrites = subject_r.lid_rewrites
80                + subject_r.cb_id_rewrites
81                + body_html_r.lid_rewrites
82                + body_html_r.cb_id_rewrites
83                + body_plain_r.lid_rewrites
84                + body_plain_r.cb_id_rewrites
85                + preheader_r
86                    .as_ref()
87                    .map(|r| r.lid_rewrites + r.cb_id_rewrites)
88                    .unwrap_or(0);
89            if total_rewrites == 0 {
90                if already_templated {
91                    summary.skipped.push(format!(
92                        "email_template '{}' already templated — skipping",
93                        et.name
94                    ));
95                }
96                continue;
97            }
98
99            for (field, warnings) in [
100                ("subject", &subject_r.warnings),
101                ("body_html", &body_html_r.warnings),
102                ("body_plaintext", &body_plain_r.warnings),
103            ] {
104                for w in warnings {
105                    summary
106                        .warnings
107                        .push(format!("email_template '{}' ({field}): {w}", et.name));
108                }
109            }
110            if let Some(r) = preheader_r.as_ref() {
111                for w in &r.warnings {
112                    summary
113                        .warnings
114                        .push(format!("email_template '{}' (preheader): {w}", et.name));
115                }
116            }
117
118            summary.touched_resources += 1;
119            summary.lid_rewrites += subject_r.lid_rewrites
120                + body_html_r.lid_rewrites
121                + body_plain_r.lid_rewrites
122                + preheader_r.as_ref().map(|r| r.lid_rewrites).unwrap_or(0);
123            summary.cb_id_rewrites += subject_r.cb_id_rewrites
124                + body_html_r.cb_id_rewrites
125                + body_plain_r.cb_id_rewrites
126                + preheader_r.as_ref().map(|r| r.cb_id_rewrites).unwrap_or(0);
127
128            et.subject = subject_r.new_body;
129            et.body_html = body_html_r.new_body;
130            et.body_plaintext = body_plain_r.new_body;
131            if let Some(r) = preheader_r {
132                et.preheader = Some(r.new_body);
133            }
134            email_template_rewrites.push(et);
135        }
136    }
137
138    eprintln!("templatize summary:");
139    eprintln!(
140        "  • touched {} resource(s); {} lid + {} cb_id rewrite(s)",
141        summary.touched_resources, summary.lid_rewrites, summary.cb_id_rewrites
142    );
143    for s in &summary.skipped {
144        eprintln!("  • {s}");
145    }
146    for w in &summary.warnings {
147        eprintln!("  ⚠ {w}");
148    }
149
150    if summary.touched_resources == 0 {
151        eprintln!("nothing to templatize.");
152        return Ok(());
153    }
154
155    if args.dry_run {
156        eprintln!(
157            "(dry-run) would rewrite {} resource file(s) in place",
158            content_block_rewrites.len() + email_template_rewrites.len()
159        );
160        return Ok(());
161    }
162
163    for (path, cb) in &content_block_rewrites {
164        content_block_io::save_content_block(path.parent().unwrap_or_else(|| Path::new(".")), cb)?;
165    }
166    for et in &email_template_rewrites {
167        email_template_io::save_email_template(&email_templates_root, et)?;
168    }
169    eprintln!(
170        "✓ templatize: rewrote {} resource file(s)",
171        content_block_rewrites.len() + email_template_rewrites.len()
172    );
173    Ok(())
174}
175
176#[derive(Default)]
177struct RunSummary {
178    touched_resources: usize,
179    lid_rewrites: usize,
180    cb_id_rewrites: usize,
181    skipped: Vec<String>,
182    warnings: Vec<String>,
183}