ant_core/node/
registry.rs1use std::collections::HashMap;
2use std::fs::File;
3use std::path::{Path, PathBuf};
4
5use fs2::FileExt;
6use serde::{Deserialize, Serialize};
7
8use crate::error::{Error, Result};
9use crate::node::types::NodeConfig;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct NodeRegistry {
14 pub schema_version: u32,
15 pub(crate) nodes: HashMap<u32, NodeConfig>,
16 pub next_id: u32,
17 #[serde(skip)]
19 pub path: PathBuf,
20}
21
22impl NodeRegistry {
23 pub fn load(path: &Path) -> Result<Self> {
25 if path.exists() {
26 let contents = std::fs::read_to_string(path)?;
27 let mut registry: Self = serde_json::from_str(&contents)?;
28 registry.path = path.to_path_buf();
29 Ok(registry)
30 } else {
31 Ok(Self {
32 schema_version: 1,
33 nodes: HashMap::new(),
34 next_id: 1,
35 path: path.to_path_buf(),
36 })
37 }
38 }
39
40 pub fn load_locked(path: &Path) -> Result<(Self, File)> {
46 if let Some(parent) = path.parent() {
47 std::fs::create_dir_all(parent)?;
48 }
49
50 let lock_path = path.with_extension("lock");
51 let lock_file = File::create(&lock_path)?;
52 lock_file.lock_exclusive()?;
53
54 let registry = Self::load(path)?;
55 Ok((registry, lock_file))
56 }
57
58 pub fn save(&self) -> Result<()> {
63 if let Some(parent) = self.path.parent() {
64 std::fs::create_dir_all(parent)?;
65 }
66 let contents = serde_json::to_string_pretty(self)?;
67 let tmp_path = self.path.with_extension("tmp");
68 std::fs::write(&tmp_path, &contents)?;
69 std::fs::rename(&tmp_path, &self.path)?;
70 Ok(())
71 }
72
73 pub fn get(&self, id: u32) -> Result<&NodeConfig> {
75 self.nodes.get(&id).ok_or(Error::NodeNotFound(id))
76 }
77
78 pub fn get_mut(&mut self, id: u32) -> Result<&mut NodeConfig> {
80 self.nodes.get_mut(&id).ok_or(Error::NodeNotFound(id))
81 }
82
83 pub fn add(&mut self, mut config: NodeConfig) -> u32 {
85 let id = self.next_id;
86 self.next_id += 1;
87 config.id = id;
88 config.service_name = format!("node{id}");
89 self.nodes.insert(id, config);
90 id
91 }
92
93 pub fn add_batch(&mut self, configs: Vec<NodeConfig>) -> Vec<u32> {
95 configs.into_iter().map(|config| self.add(config)).collect()
96 }
97
98 pub fn remove(&mut self, id: u32) -> Result<NodeConfig> {
100 self.nodes.remove(&id).ok_or(Error::NodeNotFound(id))
101 }
102
103 pub fn list(&self) -> Vec<&NodeConfig> {
105 let mut nodes: Vec<_> = self.nodes.values().collect();
106 nodes.sort_by_key(|n| n.id);
107 nodes
108 }
109
110 pub fn find_by_service_name(&self, name: &str) -> Option<&NodeConfig> {
112 self.nodes.values().find(|n| n.service_name == name)
113 }
114
115 pub fn len(&self) -> usize {
117 self.nodes.len()
118 }
119
120 pub fn is_empty(&self) -> bool {
122 self.nodes.is_empty()
123 }
124
125 pub fn clear(&mut self) {
127 self.nodes.clear();
128 self.next_id = 1;
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use std::collections::HashMap;
136 use tempfile::NamedTempFile;
137
138 fn make_config(id: u32) -> NodeConfig {
139 NodeConfig {
140 id,
141 service_name: String::new(),
142 rewards_address: "0xtest".to_string(),
143 data_dir: PathBuf::from("/tmp/test"),
144 log_dir: None,
145 node_port: None,
146 metrics_port: None,
147 network_id: None,
148 binary_path: PathBuf::from("/usr/bin/antnode"),
149 version: "0.1.0".to_string(),
150 env_variables: HashMap::new(),
151 bootstrap_peers: vec![],
152 upgrade_channel: None,
153 }
154 }
155
156 #[test]
157 fn load_creates_empty_registry() {
158 let tmp = NamedTempFile::new().unwrap();
159 let path = tmp.path().with_extension("json");
160 let reg = NodeRegistry::load(&path).unwrap();
162 assert!(reg.is_empty());
163 assert_eq!(reg.next_id, 1);
164 }
165
166 #[test]
167 fn add_and_get() {
168 let tmp = NamedTempFile::new().unwrap();
169 let path = tmp.path().with_extension("json");
170 let mut reg = NodeRegistry::load(&path).unwrap();
171 let id = reg.add(make_config(0));
172 assert_eq!(id, 1);
173 assert_eq!(reg.get(id).unwrap().rewards_address, "0xtest");
174 }
175
176 #[test]
177 fn save_and_reload() {
178 let tmp = NamedTempFile::new().unwrap();
179 let path = tmp.path().with_extension("json");
180 let mut reg = NodeRegistry::load(&path).unwrap();
181 reg.add(make_config(0));
182 reg.save().unwrap();
183
184 let reg2 = NodeRegistry::load(&path).unwrap();
185 assert_eq!(reg2.len(), 1);
186 }
187
188 #[test]
189 fn add_batch_assigns_sequential_ids() {
190 let tmp = NamedTempFile::new().unwrap();
191 let path = tmp.path().with_extension("json");
192 let mut reg = NodeRegistry::load(&path).unwrap();
193 let configs = vec![make_config(0), make_config(0), make_config(0)];
194 let ids = reg.add_batch(configs);
195 assert_eq!(ids, vec![1, 2, 3]);
196 assert_eq!(reg.len(), 3);
197 assert_eq!(reg.next_id, 4);
198 }
199
200 #[test]
201 fn load_locked_creates_lock_file() {
202 let tmp = NamedTempFile::new().unwrap();
203 let path = tmp.path().with_extension("json");
204 let (reg, _lock) = NodeRegistry::load_locked(&path).unwrap();
205 assert!(reg.is_empty());
206 assert!(path.with_extension("lock").exists());
207 }
208
209 #[test]
210 fn remove_returns_config() {
211 let tmp = NamedTempFile::new().unwrap();
212 let path = tmp.path().with_extension("json");
213 let mut reg = NodeRegistry::load(&path).unwrap();
214 let id = reg.add(make_config(0));
215 let removed = reg.remove(id).unwrap();
216 assert_eq!(removed.rewards_address, "0xtest");
217 assert!(reg.is_empty());
218 }
219
220 #[test]
221 fn remove_missing_node_errors() {
222 let tmp = NamedTempFile::new().unwrap();
223 let path = tmp.path().with_extension("json");
224 let mut reg = NodeRegistry::load(&path).unwrap();
225 let result = reg.remove(999);
226 assert!(result.is_err());
227 }
228
229 #[test]
230 fn clear_empties_registry_and_resets_next_id() {
231 let tmp = NamedTempFile::new().unwrap();
232 let path = tmp.path().with_extension("json");
233 let mut reg = NodeRegistry::load(&path).unwrap();
234 reg.add(make_config(0));
235 reg.add(make_config(0));
236 assert_eq!(reg.len(), 2);
237 assert_eq!(reg.next_id, 3);
238
239 reg.clear();
240 assert!(reg.is_empty());
241 assert_eq!(reg.next_id, 1);
242 }
243
244 #[test]
245 fn save_is_atomic_no_tmp_file_remains() {
246 let tmp = NamedTempFile::new().unwrap();
247 let path = tmp.path().with_extension("json");
248 let mut reg = NodeRegistry::load(&path).unwrap();
249 reg.add(make_config(0));
250 reg.save().unwrap();
251
252 assert!(path.exists());
254 assert!(!path.with_extension("tmp").exists());
255 }
256}