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 }
153 }
154
155 #[test]
156 fn load_creates_empty_registry() {
157 let tmp = NamedTempFile::new().unwrap();
158 let path = tmp.path().with_extension("json");
159 let reg = NodeRegistry::load(&path).unwrap();
161 assert!(reg.is_empty());
162 assert_eq!(reg.next_id, 1);
163 }
164
165 #[test]
166 fn add_and_get() {
167 let tmp = NamedTempFile::new().unwrap();
168 let path = tmp.path().with_extension("json");
169 let mut reg = NodeRegistry::load(&path).unwrap();
170 let id = reg.add(make_config(0));
171 assert_eq!(id, 1);
172 assert_eq!(reg.get(id).unwrap().rewards_address, "0xtest");
173 }
174
175 #[test]
176 fn save_and_reload() {
177 let tmp = NamedTempFile::new().unwrap();
178 let path = tmp.path().with_extension("json");
179 let mut reg = NodeRegistry::load(&path).unwrap();
180 reg.add(make_config(0));
181 reg.save().unwrap();
182
183 let reg2 = NodeRegistry::load(&path).unwrap();
184 assert_eq!(reg2.len(), 1);
185 }
186
187 #[test]
188 fn add_batch_assigns_sequential_ids() {
189 let tmp = NamedTempFile::new().unwrap();
190 let path = tmp.path().with_extension("json");
191 let mut reg = NodeRegistry::load(&path).unwrap();
192 let configs = vec![make_config(0), make_config(0), make_config(0)];
193 let ids = reg.add_batch(configs);
194 assert_eq!(ids, vec![1, 2, 3]);
195 assert_eq!(reg.len(), 3);
196 assert_eq!(reg.next_id, 4);
197 }
198
199 #[test]
200 fn load_locked_creates_lock_file() {
201 let tmp = NamedTempFile::new().unwrap();
202 let path = tmp.path().with_extension("json");
203 let (reg, _lock) = NodeRegistry::load_locked(&path).unwrap();
204 assert!(reg.is_empty());
205 assert!(path.with_extension("lock").exists());
206 }
207
208 #[test]
209 fn remove_returns_config() {
210 let tmp = NamedTempFile::new().unwrap();
211 let path = tmp.path().with_extension("json");
212 let mut reg = NodeRegistry::load(&path).unwrap();
213 let id = reg.add(make_config(0));
214 let removed = reg.remove(id).unwrap();
215 assert_eq!(removed.rewards_address, "0xtest");
216 assert!(reg.is_empty());
217 }
218
219 #[test]
220 fn remove_missing_node_errors() {
221 let tmp = NamedTempFile::new().unwrap();
222 let path = tmp.path().with_extension("json");
223 let mut reg = NodeRegistry::load(&path).unwrap();
224 let result = reg.remove(999);
225 assert!(result.is_err());
226 }
227
228 #[test]
229 fn clear_empties_registry_and_resets_next_id() {
230 let tmp = NamedTempFile::new().unwrap();
231 let path = tmp.path().with_extension("json");
232 let mut reg = NodeRegistry::load(&path).unwrap();
233 reg.add(make_config(0));
234 reg.add(make_config(0));
235 assert_eq!(reg.len(), 2);
236 assert_eq!(reg.next_id, 3);
237
238 reg.clear();
239 assert!(reg.is_empty());
240 assert_eq!(reg.next_id, 1);
241 }
242
243 #[test]
244 fn save_is_atomic_no_tmp_file_remains() {
245 let tmp = NamedTempFile::new().unwrap();
246 let path = tmp.path().with_extension("json");
247 let mut reg = NodeRegistry::load(&path).unwrap();
248 reg.add(make_config(0));
249 reg.save().unwrap();
250
251 assert!(path.exists());
253 assert!(!path.with_extension("tmp").exists());
254 }
255}