Skip to main content

force_sync/
config.rs

1//! Object-level sync configuration types.
2
3use std::collections::BTreeMap;
4
5/// Declares which system owns a field or field group.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum Owner {
8    /// Salesforce is authoritative.
9    Salesforce,
10    /// Postgres is authoritative.
11    Postgres,
12    /// Either side may update the field and conflicts must be resolved elsewhere.
13    Shared,
14}
15
16/// Conflict resolution policy for an object sync definition.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum ConflictPolicy {
19    /// Use explicit field ownership rules.
20    FieldOwnership,
21    /// Record the conflict for later manual resolution.
22    #[default]
23    ManualReview,
24}
25
26/// Transport cutovers for the planner lanes.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct LaneThresholds {
29    rest_max_batch_size: usize,
30    bulk_min_batch_size: usize,
31}
32
33impl LaneThresholds {
34    /// Returns the maximum batch size before the planner should avoid REST.
35    #[must_use]
36    pub const fn rest_max_batch_size(&self) -> usize {
37        self.rest_max_batch_size
38    }
39
40    /// Returns the minimum batch size that should prefer bulk transport.
41    #[must_use]
42    pub const fn bulk_min_batch_size(&self) -> usize {
43        self.bulk_min_batch_size
44    }
45}
46
47impl Default for LaneThresholds {
48    fn default() -> Self {
49        Self {
50            rest_max_batch_size: 25,
51            bulk_min_batch_size: 500,
52        }
53    }
54}
55
56/// Object-level sync configuration.
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct ObjectSync {
59    object_name: String,
60    external_id_field: Option<String>,
61    conflict_policy: ConflictPolicy,
62    lane_thresholds: LaneThresholds,
63    field_ownership: BTreeMap<String, Owner>,
64}
65
66impl ObjectSync {
67    /// Creates a new object sync definition.
68    #[must_use]
69    pub fn new(object_name: impl Into<String>) -> Self {
70        Self {
71            object_name: object_name.into(),
72            external_id_field: None,
73            conflict_policy: ConflictPolicy::default(),
74            lane_thresholds: LaneThresholds::default(),
75            field_ownership: BTreeMap::new(),
76        }
77    }
78
79    /// Sets the Salesforce external ID field used for canonical identity.
80    #[must_use]
81    pub fn external_id(mut self, external_id_field: impl Into<String>) -> Self {
82        self.external_id_field = Some(external_id_field.into());
83        self
84    }
85
86    /// Marks a field as owned by one side or shared.
87    #[must_use]
88    pub fn field_owner(mut self, field_name: impl Into<String>, owner: Owner) -> Self {
89        self.field_ownership.insert(field_name.into(), owner);
90        self
91    }
92
93    /// Returns the synced Salesforce object name.
94    #[must_use]
95    pub fn object_name(&self) -> &str {
96        &self.object_name
97    }
98
99    /// Returns the configured external ID field, if one is set.
100    #[must_use]
101    pub fn external_id_field(&self) -> Option<&str> {
102        self.external_id_field.as_deref()
103    }
104
105    /// Returns the conflict policy for the object.
106    #[must_use]
107    pub const fn conflict_policy(&self) -> &ConflictPolicy {
108        &self.conflict_policy
109    }
110
111    /// Returns the planner thresholds for the object.
112    #[must_use]
113    pub const fn lane_thresholds(&self) -> &LaneThresholds {
114        &self.lane_thresholds
115    }
116
117    /// Returns the ownership rule for a field, if one is configured.
118    #[must_use]
119    pub fn field_owner_for(&self, field_name: &str) -> Option<Owner> {
120        self.field_ownership.get(field_name).copied()
121    }
122
123    /// Returns the configured field ownership rules.
124    #[must_use]
125    pub const fn field_ownership(&self) -> &BTreeMap<String, Owner> {
126        &self.field_ownership
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::{ObjectSync, Owner};
133
134    #[test]
135    fn object_sync_external_id_builder_stores_config() {
136        let config = ObjectSync::new("Account").external_id("External_Id__c");
137
138        assert_eq!(config.object_name(), "Account");
139        assert_eq!(config.external_id_field(), Some("External_Id__c"));
140    }
141
142    #[test]
143    fn object_sync_field_owner_builder_stores_rules() {
144        let config = ObjectSync::new("Account").field_owner("Name", Owner::Postgres);
145
146        assert_eq!(config.field_owner_for("Name"), Some(Owner::Postgres));
147        assert_eq!(config.field_ownership().len(), 1);
148    }
149}