1use crate::braze::error::BrazeApiError;
4use crate::braze::BrazeClient;
5use crate::config::ResolvedConfig;
6use crate::fs::{catalog_io, content_block_io, email_template_io};
7use crate::resource::ResourceKind;
8use anyhow::Context as _;
9use clap::Args;
10use futures::stream::{StreamExt, TryStreamExt};
11use std::path::Path;
12
13use super::{selected_kinds, warn_unimplemented, FETCH_CONCURRENCY};
14
15#[derive(Args, Debug)]
16pub struct ExportArgs {
17 #[arg(long, value_enum)]
20 pub resource: Option<ResourceKind>,
21
22 #[arg(long, requires = "resource")]
25 pub name: Option<String>,
26}
27
28pub async fn run(
29 args: &ExportArgs,
30 resolved: ResolvedConfig,
31 config_dir: &Path,
32) -> anyhow::Result<()> {
33 let catalogs_root = config_dir.join(&resolved.resources.catalog_schema.path);
34 let content_blocks_root = config_dir.join(&resolved.resources.content_block.path);
35 let email_templates_root = config_dir.join(&resolved.resources.email_template.path);
36 let client = BrazeClient::from_resolved(&resolved);
37 let kinds = selected_kinds(args.resource, &resolved.resources);
38
39 let mut total_written: usize = 0;
40 for kind in kinds {
41 match kind {
42 ResourceKind::CatalogSchema => {
43 let n = export_catalog_schemas(&client, &catalogs_root, args.name.as_deref())
44 .await
45 .context("exporting catalog_schema")?;
46 eprintln!("✓ catalog_schema: exported {n} resource(s)");
47 total_written += n;
48 }
49 ResourceKind::ContentBlock => {
50 let n = export_content_blocks(&client, &content_blocks_root, args.name.as_deref())
51 .await
52 .context("exporting content_block")?;
53 eprintln!("✓ content_block: exported {n} resource(s)");
54 total_written += n;
55 }
56 ResourceKind::EmailTemplate => {
57 let n =
58 export_email_templates(&client, &email_templates_root, args.name.as_deref())
59 .await
60 .context("exporting email_template")?;
61 eprintln!("✓ email_template: exported {n} resource(s)");
62 total_written += n;
63 }
64 other => {
65 warn_unimplemented(other);
66 }
67 }
68 }
69
70 eprintln!("done: {total_written} resource(s) written");
71 Ok(())
72}
73
74async fn export_catalog_schemas(
75 client: &BrazeClient,
76 catalogs_root: &Path,
77 name_filter: Option<&str>,
78) -> anyhow::Result<usize> {
79 let catalogs = match name_filter {
80 Some(name) => match client.get_catalog(name).await {
81 Ok(c) => vec![c],
82 Err(BrazeApiError::NotFound { .. }) => {
84 eprintln!("⚠ catalog_schema: '{name}' not found in Braze");
85 Vec::new()
86 }
87 Err(e) => return Err(e.into()),
88 },
89 None => client.list_catalogs().await?,
90 };
91
92 let count = catalogs.len();
93 for cat in catalogs {
94 catalog_io::save_schema(catalogs_root, &cat)?;
95 }
96 Ok(count)
97}
98
99async fn export_content_blocks(
103 client: &BrazeClient,
104 content_blocks_root: &Path,
105 name_filter: Option<&str>,
106) -> anyhow::Result<usize> {
107 let summaries = client.list_content_blocks().await?;
108 let targets: Vec<_> = match name_filter {
109 Some(name) => summaries.into_iter().filter(|s| s.name == name).collect(),
110 None => summaries,
111 };
112
113 if targets.is_empty() {
114 if let Some(name) = name_filter {
115 eprintln!("⚠ content_block: '{name}' not found in Braze");
116 }
117 return Ok(0);
118 }
119
120 let blocks: Vec<crate::resource::ContentBlock> =
121 futures::stream::iter(targets.iter().map(|s| {
122 let name = s.name.as_str();
123 let id = s.content_block_id.as_str();
124 async move {
125 client
126 .get_content_block(id)
127 .await
128 .with_context(|| format!("fetching content block '{name}'"))
129 }
130 }))
131 .buffer_unordered(FETCH_CONCURRENCY)
132 .try_collect()
133 .await?;
134
135 for cb in &blocks {
136 content_block_io::save_content_block(content_blocks_root, cb)?;
137 }
138 Ok(blocks.len())
139}
140
141async fn export_email_templates(
143 client: &BrazeClient,
144 email_templates_root: &Path,
145 name_filter: Option<&str>,
146) -> anyhow::Result<usize> {
147 let summaries = client.list_email_templates().await?;
148 let targets: Vec<_> = match name_filter {
149 Some(name) => summaries.into_iter().filter(|s| s.name == name).collect(),
150 None => summaries,
151 };
152
153 if targets.is_empty() {
154 if let Some(name) = name_filter {
155 eprintln!("⚠ email_template: '{name}' not found in Braze");
156 }
157 return Ok(0);
158 }
159
160 let templates: Vec<crate::resource::EmailTemplate> =
161 futures::stream::iter(targets.iter().map(|s| {
162 let name = s.name.as_str();
163 let id = s.email_template_id.as_str();
164 async move {
165 client
166 .get_email_template(id)
167 .await
168 .with_context(|| format!("fetching email template '{name}'"))
169 }
170 }))
171 .buffer_unordered(FETCH_CONCURRENCY)
172 .try_collect()
173 .await?;
174
175 for et in &templates {
176 email_template_io::save_email_template(email_templates_root, et)?;
177 }
178 Ok(templates.len())
179}