1use std::{
2 error::Error,
3 fmt::Display,
4 net::IpAddr,
5 path::{Path, PathBuf},
6 str::FromStr,
7};
8
9use multiaddr::Multiaddr;
10use serde::{Deserialize, Serialize};
11
12use crate::{
13 shared::{
14 errors::{ConfigError, FieldError},
15 helpers::{merge_errors, merge_errors_vecs},
16 types::Duration,
17 },
18 utils::{default_as_true, default_node_spawn_timeout, default_timeout},
19};
20
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23pub struct GlobalSettings {
24 #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
26 bootnodes_addresses: Vec<Multiaddr>,
27 #[serde(rename = "timeout", default = "default_timeout")]
30 network_spawn_timeout: Duration,
31 #[serde(default = "default_node_spawn_timeout")]
34 node_spawn_timeout: Duration,
35 local_ip: Option<IpAddr>,
38 base_dir: Option<PathBuf>,
42 spawn_concurrency: Option<usize>,
44 #[serde(default = "default_as_true")]
46 tear_down_on_failure: bool,
47}
48
49impl GlobalSettings {
50 pub fn bootnodes_addresses(&self) -> Vec<&Multiaddr> {
52 self.bootnodes_addresses.iter().collect()
53 }
54
55 pub fn network_spawn_timeout(&self) -> Duration {
57 self.network_spawn_timeout
58 }
59
60 pub fn node_spawn_timeout(&self) -> Duration {
62 self.node_spawn_timeout
63 }
64
65 pub fn local_ip(&self) -> Option<&IpAddr> {
67 self.local_ip.as_ref()
68 }
69
70 pub fn base_dir(&self) -> Option<&Path> {
73 self.base_dir.as_deref()
74 }
75
76 pub fn spawn_concurrency(&self) -> Option<usize> {
78 self.spawn_concurrency
79 }
80
81 pub fn tear_down_on_failure(&self) -> bool {
83 self.tear_down_on_failure
84 }
85}
86
87impl Default for GlobalSettings {
88 fn default() -> Self {
89 Self {
90 bootnodes_addresses: Default::default(),
91 network_spawn_timeout: default_timeout(),
92 node_spawn_timeout: default_node_spawn_timeout(),
93 local_ip: Default::default(),
94 base_dir: Default::default(),
95 spawn_concurrency: Default::default(),
96 tear_down_on_failure: true,
97 }
98 }
99}
100
101#[derive(Default)]
103pub struct GlobalSettingsBuilder {
104 config: GlobalSettings,
105 errors: Vec<anyhow::Error>,
106}
107
108impl GlobalSettingsBuilder {
109 pub fn new() -> Self {
110 Self::default()
111 }
112
113 fn transition(config: GlobalSettings, errors: Vec<anyhow::Error>) -> Self {
115 Self { config, errors }
116 }
117
118 pub fn with_bootnodes_addresses<T>(self, bootnodes_addresses: Vec<T>) -> Self
120 where
121 T: TryInto<Multiaddr> + Display + Copy,
122 T::Error: Error + Send + Sync + 'static,
123 {
124 let mut addrs = vec![];
125 let mut errors = vec![];
126
127 for (index, addr) in bootnodes_addresses.into_iter().enumerate() {
128 match addr.try_into() {
129 Ok(addr) => addrs.push(addr),
130 Err(error) => errors.push(
131 FieldError::BootnodesAddress(index, addr.to_string(), error.into()).into(),
132 ),
133 }
134 }
135
136 Self::transition(
137 GlobalSettings {
138 bootnodes_addresses: addrs,
139 ..self.config
140 },
141 merge_errors_vecs(self.errors, errors),
142 )
143 }
144
145 pub fn with_network_spawn_timeout(self, timeout: Duration) -> Self {
147 Self::transition(
148 GlobalSettings {
149 network_spawn_timeout: timeout,
150 ..self.config
151 },
152 self.errors,
153 )
154 }
155
156 pub fn with_node_spawn_timeout(self, timeout: Duration) -> Self {
158 Self::transition(
159 GlobalSettings {
160 node_spawn_timeout: timeout,
161 ..self.config
162 },
163 self.errors,
164 )
165 }
166
167 pub fn with_local_ip(self, local_ip: &str) -> Self {
169 match IpAddr::from_str(local_ip) {
170 Ok(local_ip) => Self::transition(
171 GlobalSettings {
172 local_ip: Some(local_ip),
173 ..self.config
174 },
175 self.errors,
176 ),
177 Err(error) => Self::transition(
178 self.config,
179 merge_errors(self.errors, FieldError::LocalIp(error.into()).into()),
180 ),
181 }
182 }
183
184 pub fn with_base_dir(self, base_dir: impl Into<PathBuf>) -> Self {
186 Self::transition(
187 GlobalSettings {
188 base_dir: Some(base_dir.into()),
189 ..self.config
190 },
191 self.errors,
192 )
193 }
194
195 pub fn with_spawn_concurrency(self, spawn_concurrency: usize) -> Self {
197 Self::transition(
198 GlobalSettings {
199 spawn_concurrency: Some(spawn_concurrency),
200 ..self.config
201 },
202 self.errors,
203 )
204 }
205
206 pub fn with_tear_down_on_failure(self, tear_down_on_failure: bool) -> Self {
208 Self::transition(
209 GlobalSettings {
210 tear_down_on_failure,
211 ..self.config
212 },
213 self.errors,
214 )
215 }
216
217 pub fn build(self) -> Result<GlobalSettings, Vec<anyhow::Error>> {
219 if !self.errors.is_empty() {
220 return Err(self
221 .errors
222 .into_iter()
223 .map(|error| ConfigError::GlobalSettings(error).into())
224 .collect::<Vec<_>>());
225 }
226
227 Ok(self.config)
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn global_settings_config_builder_should_succeeds_and_returns_a_global_settings_config() {
237 let global_settings_config = GlobalSettingsBuilder::new()
238 .with_bootnodes_addresses(vec![
239 "/ip4/10.41.122.55/tcp/45421",
240 "/ip4/51.144.222.10/tcp/2333",
241 ])
242 .with_network_spawn_timeout(600)
243 .with_node_spawn_timeout(120)
244 .with_local_ip("10.0.0.1")
245 .with_base_dir("/home/nonroot/mynetwork")
246 .with_spawn_concurrency(5)
247 .with_tear_down_on_failure(true)
248 .build()
249 .unwrap();
250
251 let bootnodes_addresses: Vec<Multiaddr> = vec![
252 "/ip4/10.41.122.55/tcp/45421".try_into().unwrap(),
253 "/ip4/51.144.222.10/tcp/2333".try_into().unwrap(),
254 ];
255 assert_eq!(
256 global_settings_config.bootnodes_addresses(),
257 bootnodes_addresses.iter().collect::<Vec<_>>()
258 );
259 assert_eq!(global_settings_config.network_spawn_timeout(), 600);
260 assert_eq!(global_settings_config.node_spawn_timeout(), 120);
261 assert_eq!(
262 global_settings_config
263 .local_ip()
264 .unwrap()
265 .to_string()
266 .as_str(),
267 "10.0.0.1"
268 );
269 assert_eq!(
270 global_settings_config.base_dir().unwrap(),
271 Path::new("/home/nonroot/mynetwork")
272 );
273 assert_eq!(global_settings_config.spawn_concurrency().unwrap(), 5);
274 assert!(global_settings_config.tear_down_on_failure());
275 }
276
277 #[test]
278 fn global_settings_config_builder_should_succeeds_when_node_spawn_timeout_is_missing() {
279 let global_settings_config = GlobalSettingsBuilder::new()
280 .with_bootnodes_addresses(vec![
281 "/ip4/10.41.122.55/tcp/45421",
282 "/ip4/51.144.222.10/tcp/2333",
283 ])
284 .with_network_spawn_timeout(600)
285 .with_local_ip("10.0.0.1")
286 .build()
287 .unwrap();
288
289 let bootnodes_addresses: Vec<Multiaddr> = vec![
290 "/ip4/10.41.122.55/tcp/45421".try_into().unwrap(),
291 "/ip4/51.144.222.10/tcp/2333".try_into().unwrap(),
292 ];
293 assert_eq!(
294 global_settings_config.bootnodes_addresses(),
295 bootnodes_addresses.iter().collect::<Vec<_>>()
296 );
297 assert_eq!(global_settings_config.network_spawn_timeout(), 600);
298 assert_eq!(global_settings_config.node_spawn_timeout(), 600);
299 assert_eq!(
300 global_settings_config
301 .local_ip()
302 .unwrap()
303 .to_string()
304 .as_str(),
305 "10.0.0.1"
306 );
307 }
308
309 #[test]
310 fn global_settings_builder_should_fails_and_returns_an_error_if_one_bootnode_address_is_invalid(
311 ) {
312 let errors = GlobalSettingsBuilder::new()
313 .with_bootnodes_addresses(vec!["/ip4//tcp/45421"])
314 .build()
315 .unwrap_err();
316
317 assert_eq!(errors.len(), 1);
318 assert_eq!(
319 errors.first().unwrap().to_string(),
320 "global_settings.bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
321 );
322 }
323
324 #[test]
325 fn global_settings_builder_should_fails_and_returns_multiple_errors_if_multiple_bootnodes_addresses_are_invalid(
326 ) {
327 let errors = GlobalSettingsBuilder::new()
328 .with_bootnodes_addresses(vec!["/ip4//tcp/45421", "//10.42.153.10/tcp/43111"])
329 .build()
330 .unwrap_err();
331
332 assert_eq!(errors.len(), 2);
333 assert_eq!(
334 errors.first().unwrap().to_string(),
335 "global_settings.bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
336 );
337 assert_eq!(
338 errors.get(1).unwrap().to_string(),
339 "global_settings.bootnodes_addresses[1]: '//10.42.153.10/tcp/43111' unknown protocol string: "
340 );
341 }
342
343 #[test]
344 fn global_settings_builder_should_fails_and_returns_an_error_if_local_ip_is_invalid() {
345 let errors = GlobalSettingsBuilder::new()
346 .with_local_ip("invalid")
347 .build()
348 .unwrap_err();
349
350 assert_eq!(errors.len(), 1);
351 assert_eq!(
352 errors.first().unwrap().to_string(),
353 "global_settings.local_ip: invalid IP address syntax"
354 );
355 }
356
357 #[test]
358 fn global_settings_builder_should_fails_and_returns_multiple_errors_if_multiple_fields_are_invalid(
359 ) {
360 let errors = GlobalSettingsBuilder::new()
361 .with_bootnodes_addresses(vec!["/ip4//tcp/45421", "//10.42.153.10/tcp/43111"])
362 .with_local_ip("invalid")
363 .build()
364 .unwrap_err();
365
366 assert_eq!(errors.len(), 3);
367 assert_eq!(
368 errors.first().unwrap().to_string(),
369 "global_settings.bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
370 );
371 assert_eq!(
372 errors.get(1).unwrap().to_string(),
373 "global_settings.bootnodes_addresses[1]: '//10.42.153.10/tcp/43111' unknown protocol string: "
374 );
375 assert_eq!(
376 errors.get(2).unwrap().to_string(),
377 "global_settings.local_ip: invalid IP address syntax"
378 );
379 }
380}