1use crate::braze::error::BrazeApiError;
15use crate::braze::BrazeClient;
16use crate::config::ResolvedConfig;
17use crate::diff::catalog::diff_schema;
18use crate::diff::{DiffSummary, ResourceDiff};
19use crate::error::Error;
20use crate::format::OutputFormat;
21use crate::fs::catalog_io;
22use crate::resource::{Catalog, ResourceKind};
23use anyhow::Context as _;
24use clap::Args;
25use std::collections::{BTreeMap, BTreeSet};
26use std::path::Path;
27
28use super::{selected_kinds, warn_unimplemented};
29
30#[derive(Args, Debug)]
31pub struct DiffArgs {
32 #[arg(long, value_enum)]
34 pub resource: Option<ResourceKind>,
35
36 #[arg(long, requires = "resource")]
39 pub name: Option<String>,
40
41 #[arg(long)]
43 pub fail_on_drift: bool,
44}
45
46pub async fn run(
47 args: &DiffArgs,
48 resolved: ResolvedConfig,
49 config_dir: &Path,
50 format: OutputFormat,
51) -> anyhow::Result<()> {
52 let catalogs_root = config_dir.join(&resolved.resources.catalog_schema.path);
53 let client = BrazeClient::from_resolved(&resolved);
54 let kinds = selected_kinds(args.resource, &resolved.resources);
55
56 let mut summary = DiffSummary::default();
57 for kind in kinds {
58 match kind {
59 ResourceKind::CatalogSchema => {
60 let diffs =
61 compute_catalog_schema_diffs(&client, &catalogs_root, args.name.as_deref())
62 .await
63 .context("computing catalog_schema diff")?;
64 summary.diffs.extend(diffs);
65 }
66 other => {
67 warn_unimplemented(other);
68 }
69 }
70 }
71
72 let formatted = format.formatter().format(&summary);
75 print!("{formatted}");
76
77 if args.fail_on_drift && summary.changed_count() > 0 {
78 return Err(Error::DriftDetected {
79 count: summary.changed_count(),
80 }
81 .into());
82 }
83
84 Ok(())
85}
86
87pub(crate) async fn compute_catalog_schema_diffs(
93 client: &BrazeClient,
94 catalogs_root: &Path,
95 name_filter: Option<&str>,
96) -> anyhow::Result<Vec<ResourceDiff>> {
97 let mut local = catalog_io::load_all_schemas(catalogs_root)?;
99 if let Some(name) = name_filter {
100 local.retain(|c| c.name == name);
101 }
102
103 let remote: Vec<Catalog> = match name_filter {
105 Some(name) => match client.get_catalog(name).await {
106 Ok(c) => vec![c],
107 Err(BrazeApiError::NotFound { .. }) => Vec::new(),
111 Err(e) => return Err(e.into()),
112 },
113 None => client.list_catalogs().await?,
114 };
115
116 let local_by_name: BTreeMap<&str, &Catalog> =
119 local.iter().map(|c| (c.name.as_str(), c)).collect();
120 let remote_by_name: BTreeMap<&str, &Catalog> =
121 remote.iter().map(|c| (c.name.as_str(), c)).collect();
122
123 let mut all_names: BTreeSet<&str> = BTreeSet::new();
124 all_names.extend(local_by_name.keys().copied());
125 all_names.extend(remote_by_name.keys().copied());
126
127 let mut diffs = Vec::new();
128 for name in all_names {
129 let l = local_by_name.get(name).copied();
130 let r = remote_by_name.get(name).copied();
131 if let Some(d) = diff_schema(l, r) {
132 diffs.push(ResourceDiff::CatalogSchema(d));
133 }
134 }
135
136 Ok(diffs)
137}