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