Skip to main content

closure_tree/
config.rs

1use crc32fast::Hasher;
2
3/// Static configuration describing how a SeaORM model integrates with
4/// the closure-table hierarchy.
5#[derive(Clone, Debug)]
6pub struct ClosureTreeConfig {
7    entity_name: String,
8    hierarchy_name: String,
9    parent_column: String,
10    name_column: String,
11    hierarchy_table: String,
12    dependent_behavior: DependentBehavior,
13    order_strategy: Option<OrderStrategy>,
14    advisory_lock_strategy: AdvisoryLockStrategy,
15}
16
17impl ClosureTreeConfig {
18    /// Create a new configuration using the logical entity and hierarchy names.
19    pub fn new(entity_name: impl Into<String>, hierarchy_name: impl Into<String>) -> Self {
20        let entity_name = entity_name.into();
21        let hierarchy_name = hierarchy_name.into();
22
23        let default_lock = AdvisoryLockStrategy::Namespaced(AdvisoryLockKey::derived_from(
24            &entity_name,
25            &hierarchy_name,
26        ));
27
28        Self {
29            entity_name,
30            hierarchy_name,
31            parent_column: "parent_id".to_string(),
32            name_column: "name".to_string(),
33            hierarchy_table: String::new(),
34            dependent_behavior: DependentBehavior::default(),
35            order_strategy: None,
36            advisory_lock_strategy: default_lock,
37        }
38    }
39
40    /// Merge options produced by [`ClosureTreeOptions`].
41    pub(crate) fn apply_options(mut self, options: ClosureTreeOptions) -> Self {
42        if let Some(parent_column) = options.parent_column {
43            self.parent_column = parent_column;
44        }
45        if let Some(name_column) = options.name_column {
46            self.name_column = name_column;
47        }
48        if let Some(hierarchy_table) = options.hierarchy_table {
49            self.hierarchy_table = hierarchy_table;
50        }
51        if let Some(behavior) = options.dependent_behavior {
52            self.dependent_behavior = behavior;
53        }
54        if let Some(order_strategy) = options.order_strategy {
55            self.order_strategy = Some(order_strategy);
56        }
57        if let Some(strategy) = options.advisory_lock_strategy {
58            self.advisory_lock_strategy = strategy;
59        }
60        self
61    }
62
63    /// Human-readable Rust struct name for the base entity.
64    pub fn entity_name(&self) -> &str {
65        &self.entity_name
66    }
67
68    /// Associated SeaORM entity name for the hierarchy model.
69    pub fn hierarchy_name(&self) -> &str {
70        &self.hierarchy_name
71    }
72
73    /// Column name storing the parent foreign key.
74    pub fn parent_column(&self) -> &str {
75        &self.parent_column
76    }
77
78    /// Column name storing the display name.
79    pub fn name_column(&self) -> &str {
80        &self.name_column
81    }
82
83    /// Table backing the hierarchy entity.
84    pub fn hierarchy_table(&self) -> &str {
85        &self.hierarchy_table
86    }
87
88    /// Dependent behavior when deleting nodes.
89    pub fn dependent_behavior(&self) -> DependentBehavior {
90        self.dependent_behavior
91    }
92
93    /// Ordering strategy to apply when returning descendants.
94    pub fn order_strategy(&self) -> Option<&OrderStrategy> {
95        self.order_strategy.as_ref()
96    }
97
98    /// Advisory lock strategy (PostgreSQL only).
99    pub fn advisory_lock_strategy(&self) -> &AdvisoryLockStrategy {
100        &self.advisory_lock_strategy
101    }
102}
103
104/// Builder-style options consumed by the derive macro.
105#[derive(Clone, Debug, Default)]
106pub struct ClosureTreeOptions {
107    parent_column: Option<String>,
108    name_column: Option<String>,
109    hierarchy_table: Option<String>,
110    dependent_behavior: Option<DependentBehavior>,
111    order_strategy: Option<OrderStrategy>,
112    advisory_lock_strategy: Option<AdvisoryLockStrategy>,
113}
114
115impl ClosureTreeOptions {
116    pub fn parent_column(mut self, value: impl Into<String>) -> Self {
117        self.parent_column = Some(value.into());
118        self
119    }
120
121    pub fn name_column(mut self, value: impl Into<String>) -> Self {
122        self.name_column = Some(value.into());
123        self
124    }
125
126    pub fn hierarchy_table(mut self, value: impl Into<String>) -> Self {
127        self.hierarchy_table = Some(value.into());
128        self
129    }
130
131    pub fn dependent_behavior(mut self, behavior: DependentBehavior) -> Self {
132        self.dependent_behavior = Some(behavior);
133        self
134    }
135
136    pub fn order_strategy(mut self, strategy: OrderStrategy) -> Self {
137        self.order_strategy = Some(strategy);
138        self
139    }
140
141    pub fn advisory_lock_strategy(mut self, strategy: AdvisoryLockStrategy) -> Self {
142        self.advisory_lock_strategy = Some(strategy);
143        self
144    }
145
146    pub fn apply(self, base: ClosureTreeConfig) -> ClosureTreeConfig {
147        base.apply_options(self)
148    }
149}
150
151/// Behaviour to apply to dependent nodes when destroying a record.
152#[derive(Copy, Clone, Debug, Eq, PartialEq)]
153pub enum DependentBehavior {
154    Nullify,
155    Destroy,
156    DeleteAll,
157    None,
158}
159
160impl Default for DependentBehavior {
161    fn default() -> Self {
162        Self::Nullify
163    }
164}
165
166/// Strategy used to generate deterministic ordering.
167#[derive(Clone, Debug, Eq, PartialEq)]
168pub enum OrderStrategy {
169    Manual,
170    NumericColumn { column: String },
171}
172
173impl OrderStrategy {
174    pub fn numeric_column(column: impl Into<String>) -> Self {
175        Self::NumericColumn {
176            column: column.into(),
177        }
178    }
179}
180
181/// Key used for PostgreSQL advisory locks.
182#[derive(Clone, Debug, Eq, PartialEq, Hash)]
183pub struct AdvisoryLockKey(String);
184
185impl AdvisoryLockKey {
186    pub fn new(value: impl Into<String>) -> Self {
187        Self(value.into())
188    }
189
190    pub fn as_str(&self) -> &str {
191        self.0.as_str()
192    }
193
194    fn derived_from(entity: &str, hierarchy: &str) -> Self {
195        let mut hasher = Hasher::new();
196        hasher.update(entity.as_bytes());
197        hasher.update(b"/");
198        hasher.update(hierarchy.as_bytes());
199        let crc = hasher.finalize();
200        Self(format!("closure-tree::{entity}::{hierarchy}::{crc:x}"))
201    }
202}
203
204/// Configuration describing how to acquire advisory locks.
205#[derive(Clone, Debug, Eq, PartialEq)]
206pub enum AdvisoryLockStrategy {
207    Disabled,
208    Namespaced(AdvisoryLockKey),
209}
210
211impl AdvisoryLockStrategy {
212    pub fn key(&self) -> Option<&AdvisoryLockKey> {
213        match self {
214            AdvisoryLockStrategy::Disabled => None,
215            AdvisoryLockStrategy::Namespaced(key) => Some(key),
216        }
217    }
218}