1use crate::braze::error::BrazeApiError;
4use crate::braze::BrazeClient;
5use crate::config::ResolvedConfig;
6use crate::fs::{catalog_io, content_block_io, custom_attribute_io, email_template_io};
7use crate::resource::{CustomAttributeRegistry, ResourceKind};
8use anyhow::Context as _;
9use clap::Args;
10use futures::stream::{StreamExt, TryStreamExt};
11use std::path::Path;
12
13use super::diff::resolve_catalog_names;
14use super::{selected_kinds, FETCH_CONCURRENCY};
15
16#[derive(Args, Debug, Default)]
17pub struct ExportArgs {
18 #[arg(long, value_enum)]
21 pub resource: Option<ResourceKind>,
22
23 #[arg(long, requires = "resource")]
26 pub name: Option<String>,
27}
28
29pub async fn run(
30 args: &ExportArgs,
31 resolved: ResolvedConfig,
32 config_dir: &Path,
33) -> anyhow::Result<()> {
34 let catalogs_root = config_dir.join(&resolved.resources.catalog_schema.path);
35 let content_blocks_root = config_dir.join(&resolved.resources.content_block.path);
36 let email_templates_root = config_dir.join(&resolved.resources.email_template.path);
37 let custom_attributes_path = config_dir.join(&resolved.resources.custom_attribute.path);
38 let client = BrazeClient::from_resolved(&resolved);
39 let kinds = selected_kinds(args.resource, &resolved.resources);
40
41 let mut total_written: usize = 0;
42 for kind in kinds {
43 match kind {
44 ResourceKind::CatalogSchema => {
45 let n = export_catalog_schemas(&client, &catalogs_root, args.name.as_deref())
46 .await
47 .context("exporting catalog_schema")?;
48 eprintln!("✓ catalog_schema: exported {n} resource(s)");
49 total_written += n;
50 }
51 ResourceKind::CatalogItems => {
52 let n = export_catalog_items(&client, &catalogs_root, args.name.as_deref())
53 .await
54 .context("exporting catalog_items")?;
55 eprintln!("✓ catalog_items: exported {n} catalog(s)");
56 total_written += n;
57 }
58 ResourceKind::ContentBlock => {
59 let n = export_content_blocks(&client, &content_blocks_root, args.name.as_deref())
60 .await
61 .context("exporting content_block")?;
62 eprintln!("✓ content_block: exported {n} resource(s)");
63 total_written += n;
64 }
65 ResourceKind::EmailTemplate => {
66 let n =
67 export_email_templates(&client, &email_templates_root, args.name.as_deref())
68 .await
69 .context("exporting email_template")?;
70 eprintln!("✓ email_template: exported {n} resource(s)");
71 total_written += n;
72 }
73 ResourceKind::CustomAttribute => {
74 if args.name.is_some() {
75 eprintln!(
76 "⚠ custom_attribute: --name is not supported for export \
77 (the registry is a single file); exporting all attributes"
78 );
79 }
80 let n = export_custom_attributes(&client, &custom_attributes_path)
81 .await
82 .context("exporting custom_attribute")?;
83 eprintln!("✓ custom_attribute: exported {n} attribute(s)");
84 total_written += n;
85 }
86 }
87 }
88
89 eprintln!("done: {total_written} resource(s) written");
90 Ok(())
91}
92
93async fn export_catalog_schemas(
94 client: &BrazeClient,
95 catalogs_root: &Path,
96 name_filter: Option<&str>,
97) -> anyhow::Result<usize> {
98 let catalogs = match name_filter {
99 Some(name) => match client.get_catalog(name).await {
100 Ok(c) => vec![c],
101 Err(BrazeApiError::NotFound { .. }) => {
103 eprintln!("⚠ catalog_schema: '{name}' not found in Braze");
104 Vec::new()
105 }
106 Err(e) => return Err(e.into()),
107 },
108 None => client.list_catalogs().await?,
109 };
110
111 let count = catalogs.len();
112 for cat in catalogs {
113 catalog_io::save_schema(catalogs_root, &cat)?;
114 }
115 Ok(count)
116}
117
118async fn export_content_blocks(
122 client: &BrazeClient,
123 content_blocks_root: &Path,
124 name_filter: Option<&str>,
125) -> anyhow::Result<usize> {
126 let summaries = client.list_content_blocks().await?;
127 let targets: Vec<_> = match name_filter {
128 Some(name) => summaries.into_iter().filter(|s| s.name == name).collect(),
129 None => summaries,
130 };
131
132 if targets.is_empty() {
133 if let Some(name) = name_filter {
134 eprintln!("⚠ content_block: '{name}' not found in Braze");
135 }
136 return Ok(0);
137 }
138
139 let blocks: Vec<crate::resource::ContentBlock> =
140 futures::stream::iter(targets.iter().map(|s| {
141 let name = s.name.as_str();
142 let id = s.content_block_id.as_str();
143 async move {
144 client
145 .get_content_block(id)
146 .await
147 .with_context(|| format!("fetching content block '{name}'"))
148 }
149 }))
150 .buffer_unordered(FETCH_CONCURRENCY)
151 .try_collect()
152 .await?;
153
154 for cb in &blocks {
155 content_block_io::save_content_block(content_blocks_root, cb)?;
156 }
157 Ok(blocks.len())
158}
159
160async fn export_email_templates(
162 client: &BrazeClient,
163 email_templates_root: &Path,
164 name_filter: Option<&str>,
165) -> anyhow::Result<usize> {
166 let summaries = client.list_email_templates().await?;
167 let targets: Vec<_> = match name_filter {
168 Some(name) => summaries.into_iter().filter(|s| s.name == name).collect(),
169 None => summaries,
170 };
171
172 if targets.is_empty() {
173 if let Some(name) = name_filter {
174 eprintln!("⚠ email_template: '{name}' not found in Braze");
175 }
176 return Ok(0);
177 }
178
179 let templates: Vec<crate::resource::EmailTemplate> =
180 futures::stream::iter(targets.iter().map(|s| {
181 let name = s.name.as_str();
182 let id = s.email_template_id.as_str();
183 async move {
184 client
185 .get_email_template(id)
186 .await
187 .with_context(|| format!("fetching email template '{name}'"))
188 }
189 }))
190 .buffer_unordered(FETCH_CONCURRENCY)
191 .try_collect()
192 .await?;
193
194 for et in &templates {
195 email_template_io::save_email_template(email_templates_root, et)?;
196 }
197 Ok(templates.len())
198}
199
200async fn export_catalog_items(
204 client: &BrazeClient,
205 catalogs_root: &Path,
206 name_filter: Option<&str>,
207) -> anyhow::Result<usize> {
208 let catalog_names = resolve_catalog_names(client, name_filter).await?;
209
210 let mut stream = futures::stream::iter(catalog_names.into_iter().map(|name| {
211 let client = client.clone();
212 async move {
213 match client.list_catalog_items(&name).await {
214 Ok(items) => Ok(Some((name, items))),
215 Err(BrazeApiError::NotFound { .. }) => {
216 eprintln!("⚠ catalog_items: catalog '{name}' not found in Braze");
217 Ok(None)
218 }
219 Err(e) => Err(e),
220 }
221 }
222 }))
223 .buffer_unordered(FETCH_CONCURRENCY);
224
225 let mut count = 0;
226 while let Some(result) = stream.next().await {
227 if let Some((name, items)) = result? {
228 catalog_io::save_items(catalogs_root, &name, &items)?;
229 count += 1;
230 }
231 }
232 Ok(count)
233}
234
235async fn export_custom_attributes(
236 client: &BrazeClient,
237 registry_path: &Path,
238) -> anyhow::Result<usize> {
239 let attrs = client.list_custom_attributes().await?;
240 let count = attrs.len();
241 let registry = CustomAttributeRegistry { attributes: attrs };
242 custom_attribute_io::save_registry(registry_path, ®istry)?;
243 Ok(count)
244}