1use crate::braze::error::BrazeApiError;
4use crate::braze::BrazeClient;
5use crate::config::ResolvedConfig;
6use crate::fs::{catalog_io, content_block_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 client = BrazeClient::from_resolved(&resolved);
36 let kinds = selected_kinds(args.resource, &resolved.resources);
37
38 let mut total_written: usize = 0;
39 for kind in kinds {
40 match kind {
41 ResourceKind::CatalogSchema => {
42 let n = export_catalog_schemas(&client, &catalogs_root, args.name.as_deref())
43 .await
44 .context("exporting catalog_schema")?;
45 eprintln!("✓ catalog_schema: exported {n} resource(s)");
46 total_written += n;
47 }
48 ResourceKind::ContentBlock => {
49 let n = export_content_blocks(&client, &content_blocks_root, args.name.as_deref())
50 .await
51 .context("exporting content_block")?;
52 eprintln!("✓ content_block: exported {n} resource(s)");
53 total_written += n;
54 }
55 other => {
56 warn_unimplemented(other);
57 }
58 }
59 }
60
61 eprintln!("done: {total_written} resource(s) written");
62 Ok(())
63}
64
65async fn export_catalog_schemas(
66 client: &BrazeClient,
67 catalogs_root: &Path,
68 name_filter: Option<&str>,
69) -> anyhow::Result<usize> {
70 let catalogs = match name_filter {
71 Some(name) => match client.get_catalog(name).await {
72 Ok(c) => vec![c],
73 Err(BrazeApiError::NotFound { .. }) => {
75 eprintln!("⚠ catalog_schema: '{name}' not found in Braze");
76 Vec::new()
77 }
78 Err(e) => return Err(e.into()),
79 },
80 None => client.list_catalogs().await?,
81 };
82
83 let count = catalogs.len();
84 for cat in catalogs {
85 catalog_io::save_schema(catalogs_root, &cat)?;
86 }
87 Ok(count)
88}
89
90async fn export_content_blocks(
94 client: &BrazeClient,
95 content_blocks_root: &Path,
96 name_filter: Option<&str>,
97) -> anyhow::Result<usize> {
98 let summaries = client.list_content_blocks().await?;
99 let targets: Vec<_> = match name_filter {
100 Some(name) => summaries.into_iter().filter(|s| s.name == name).collect(),
101 None => summaries,
102 };
103
104 if targets.is_empty() {
105 if let Some(name) = name_filter {
106 eprintln!("⚠ content_block: '{name}' not found in Braze");
107 }
108 return Ok(0);
109 }
110
111 let blocks: Vec<crate::resource::ContentBlock> =
112 futures::stream::iter(targets.iter().map(|s| {
113 let name = s.name.as_str();
114 let id = s.content_block_id.as_str();
115 async move {
116 client
117 .get_content_block(id)
118 .await
119 .with_context(|| format!("fetching content block '{name}'"))
120 }
121 }))
122 .buffer_unordered(FETCH_CONCURRENCY)
123 .try_collect()
124 .await?;
125
126 for cb in &blocks {
127 content_block_io::save_content_block(content_blocks_root, cb)?;
128 }
129 Ok(blocks.len())
130}