Skip to main content

braze_sync/diff/
mod.rs

1//! Pure structural diff layer. No I/O.
2//!
3//! See IMPLEMENTATION.md §6.6 and §11. The shape of [`DiffOp`] and
4//! [`ResourceDiff`] is the central design contract: every diff site in the
5//! crate goes through these types so that adding a resource forces all
6//! match arms to be updated by the compiler.
7
8use crate::resource::ResourceKind;
9
10pub mod catalog;
11pub mod content_block;
12pub mod custom_attribute;
13pub mod email_template;
14
15/// A diff operation on a single entity. Polymorphic over the entity type so
16/// the same vocabulary applies to whole resources, individual fields, etc.
17#[derive(Debug, Clone, PartialEq)]
18pub enum DiffOp<T> {
19    Added(T),
20    Removed(T),
21    Modified { from: T, to: T },
22    Unchanged,
23}
24
25impl<T> DiffOp<T> {
26    pub fn is_change(&self) -> bool {
27        !matches!(self, Self::Unchanged)
28    }
29
30    pub fn is_destructive(&self) -> bool {
31        matches!(self, Self::Removed(_))
32    }
33}
34
35/// Per-resource-kind diff result.
36#[derive(Debug, Clone)]
37pub enum ResourceDiff {
38    CatalogSchema(catalog::CatalogSchemaDiff),
39    CatalogItems(catalog::CatalogItemsDiff),
40    ContentBlock(content_block::ContentBlockDiff),
41    EmailTemplate(email_template::EmailTemplateDiff),
42    CustomAttribute(custom_attribute::CustomAttributeDiff),
43}
44
45impl ResourceDiff {
46    pub fn kind(&self) -> ResourceKind {
47        match self {
48            Self::CatalogSchema(_) => ResourceKind::CatalogSchema,
49            Self::CatalogItems(_) => ResourceKind::CatalogItems,
50            Self::ContentBlock(_) => ResourceKind::ContentBlock,
51            Self::EmailTemplate(_) => ResourceKind::EmailTemplate,
52            Self::CustomAttribute(_) => ResourceKind::CustomAttribute,
53        }
54    }
55
56    pub fn name(&self) -> &str {
57        match self {
58            Self::CatalogSchema(d) => &d.name,
59            Self::CatalogItems(d) => &d.catalog_name,
60            Self::ContentBlock(d) => &d.name,
61            Self::EmailTemplate(d) => &d.name,
62            Self::CustomAttribute(d) => &d.name,
63        }
64    }
65
66    pub fn has_changes(&self) -> bool {
67        match self {
68            Self::CatalogSchema(d) => d.has_changes(),
69            Self::CatalogItems(d) => d.has_changes(),
70            Self::ContentBlock(d) => d.has_changes(),
71            Self::EmailTemplate(d) => d.has_changes(),
72            Self::CustomAttribute(d) => d.has_changes(),
73        }
74    }
75
76    pub fn has_destructive(&self) -> bool {
77        match self {
78            Self::CatalogSchema(d) => d.has_destructive(),
79            Self::CatalogItems(d) => d.has_destructive(),
80            // Content Block / Email Template have no DELETE API. "Destructive"
81            // for these resources is reframed as orphan tracking (§11.6); the
82            // apply path performs no destructive call.
83            Self::ContentBlock(_) => false,
84            Self::EmailTemplate(_) => false,
85            // Custom Attribute "removal" is only a deprecation flag toggle.
86            Self::CustomAttribute(_) => false,
87        }
88    }
89
90    pub fn is_orphan(&self) -> bool {
91        match self {
92            Self::ContentBlock(d) => d.is_orphan(),
93            Self::EmailTemplate(d) => d.is_orphan(),
94            _ => false,
95        }
96    }
97}
98
99#[derive(Debug, Clone, Default)]
100pub struct DiffSummary {
101    pub diffs: Vec<ResourceDiff>,
102}
103
104impl DiffSummary {
105    pub fn changed_count(&self) -> usize {
106        self.diffs.iter().filter(|d| d.has_changes()).count()
107    }
108
109    pub fn destructive_count(&self) -> usize {
110        self.diffs.iter().filter(|d| d.has_destructive()).count()
111    }
112
113    pub fn orphan_count(&self) -> usize {
114        self.diffs.iter().filter(|d| d.is_orphan()).count()
115    }
116
117    pub fn in_sync_count(&self) -> usize {
118        self.diffs.iter().filter(|d| !d.has_changes()).count()
119    }
120}