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