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_raw_bootnodes_addresses<T>(self, bootnodes_addresses: Vec<T>) -> Self
123 where
124 T: TryInto<Multiaddr> + Display + Copy,
125 T::Error: Error + Send + Sync + 'static,
126 {
127 let mut addrs = vec![];
128 let mut errors = vec![];
129
130 for (index, addr) in bootnodes_addresses.into_iter().enumerate() {
131 match addr.try_into() {
132 Ok(addr) => addrs.push(addr),
133 Err(error) => errors.push(
134 FieldError::BootnodesAddress(index, addr.to_string(), error.into()).into(),
135 ),
136 }
137 }
138
139 Self::transition(
140 GlobalSettings {
141 bootnodes_addresses: addrs,
142 ..self.config
143 },
144 merge_errors_vecs(self.errors, errors),
145 )
146 }
147
148 pub fn with_network_spawn_timeout(self, timeout: Duration) -> Self {
150 Self::transition(
151 GlobalSettings {
152 network_spawn_timeout: timeout,
153 ..self.config
154 },
155 self.errors,
156 )
157 }
158
159 pub fn with_node_spawn_timeout(self, timeout: Duration) -> Self {
161 Self::transition(
162 GlobalSettings {
163 node_spawn_timeout: timeout,
164 ..self.config
165 },
166 self.errors,
167 )
168 }
169
170 pub fn with_local_ip(self, local_ip: &str) -> Self {
172 match IpAddr::from_str(local_ip) {
173 Ok(local_ip) => Self::transition(
174 GlobalSettings {
175 local_ip: Some(local_ip),
176 ..self.config
177 },
178 self.errors,
179 ),
180 Err(error) => Self::transition(
181 self.config,
182 merge_errors(self.errors, FieldError::LocalIp(error.into()).into()),
183 ),
184 }
185 }
186
187 pub fn with_base_dir(self, base_dir: impl Into<PathBuf>) -> Self {
189 Self::transition(
190 GlobalSettings {
191 base_dir: Some(base_dir.into()),
192 ..self.config
193 },
194 self.errors,
195 )
196 }
197
198 pub fn with_spawn_concurrency(self, spawn_concurrency: usize) -> Self {
200 Self::transition(
201 GlobalSettings {
202 spawn_concurrency: Some(spawn_concurrency),
203 ..self.config
204 },
205 self.errors,
206 )
207 }
208
209 pub fn with_tear_down_on_failure(self, tear_down_on_failure: bool) -> Self {
211 Self::transition(
212 GlobalSettings {
213 tear_down_on_failure,
214 ..self.config
215 },
216 self.errors,
217 )
218 }
219
220 pub fn build(self) -> Result<GlobalSettings, Vec<anyhow::Error>> {
222 if !self.errors.is_empty() {
223 return Err(self
224 .errors
225 .into_iter()
226 .map(|error| ConfigError::GlobalSettings(error).into())
227 .collect::<Vec<_>>());
228 }
229
230 Ok(self.config)
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237
238 #[test]
239 fn global_settings_config_builder_should_succeeds_and_returns_a_global_settings_config() {
240 let global_settings_config = GlobalSettingsBuilder::new()
241 .with_raw_bootnodes_addresses(vec![
242 "/ip4/10.41.122.55/tcp/45421",
243 "/ip4/51.144.222.10/tcp/2333",
244 ])
245 .with_network_spawn_timeout(600)
246 .with_node_spawn_timeout(120)
247 .with_local_ip("10.0.0.1")
248 .with_base_dir("/home/nonroot/mynetwork")
249 .with_spawn_concurrency(5)
250 .with_tear_down_on_failure(true)
251 .build()
252 .unwrap();
253
254 let bootnodes_addresses: Vec<Multiaddr> = vec![
255 "/ip4/10.41.122.55/tcp/45421".try_into().unwrap(),
256 "/ip4/51.144.222.10/tcp/2333".try_into().unwrap(),
257 ];
258 assert_eq!(
259 global_settings_config.bootnodes_addresses(),
260 bootnodes_addresses.iter().collect::<Vec<_>>()
261 );
262 assert_eq!(global_settings_config.network_spawn_timeout(), 600);
263 assert_eq!(global_settings_config.node_spawn_timeout(), 120);
264 assert_eq!(
265 global_settings_config
266 .local_ip()
267 .unwrap()
268 .to_string()
269 .as_str(),
270 "10.0.0.1"
271 );
272 assert_eq!(
273 global_settings_config.base_dir().unwrap(),
274 Path::new("/home/nonroot/mynetwork")
275 );
276 assert_eq!(global_settings_config.spawn_concurrency().unwrap(), 5);
277 assert!(global_settings_config.tear_down_on_failure());
278 }
279
280 #[test]
281 fn global_settings_config_builder_should_succeeds_when_node_spawn_timeout_is_missing() {
282 let global_settings_config = GlobalSettingsBuilder::new()
283 .with_raw_bootnodes_addresses(vec![
284 "/ip4/10.41.122.55/tcp/45421",
285 "/ip4/51.144.222.10/tcp/2333",
286 ])
287 .with_network_spawn_timeout(600)
288 .with_local_ip("10.0.0.1")
289 .build()
290 .unwrap();
291
292 let bootnodes_addresses: Vec<Multiaddr> = vec![
293 "/ip4/10.41.122.55/tcp/45421".try_into().unwrap(),
294 "/ip4/51.144.222.10/tcp/2333".try_into().unwrap(),
295 ];
296 assert_eq!(
297 global_settings_config.bootnodes_addresses(),
298 bootnodes_addresses.iter().collect::<Vec<_>>()
299 );
300 assert_eq!(global_settings_config.network_spawn_timeout(), 600);
301 assert_eq!(global_settings_config.node_spawn_timeout(), 600);
302 assert_eq!(
303 global_settings_config
304 .local_ip()
305 .unwrap()
306 .to_string()
307 .as_str(),
308 "10.0.0.1"
309 );
310 }
311
312 #[test]
313 fn global_settings_builder_should_fails_and_returns_an_error_if_one_bootnode_address_is_invalid(
314 ) {
315 let errors = GlobalSettingsBuilder::new()
316 .with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421"])
317 .build()
318 .unwrap_err();
319
320 assert_eq!(errors.len(), 1);
321 assert_eq!(
322 errors.first().unwrap().to_string(),
323 "global_settings.bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
324 );
325 }
326
327 #[test]
328 fn global_settings_builder_should_fails_and_returns_multiple_errors_if_multiple_bootnodes_addresses_are_invalid(
329 ) {
330 let errors = GlobalSettingsBuilder::new()
331 .with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421", "//10.42.153.10/tcp/43111"])
332 .build()
333 .unwrap_err();
334
335 assert_eq!(errors.len(), 2);
336 assert_eq!(
337 errors.first().unwrap().to_string(),
338 "global_settings.bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
339 );
340 assert_eq!(
341 errors.get(1).unwrap().to_string(),
342 "global_settings.bootnodes_addresses[1]: '//10.42.153.10/tcp/43111' unknown protocol string: "
343 );
344 }
345
346 #[test]
347 fn global_settings_builder_should_fails_and_returns_an_error_if_local_ip_is_invalid() {
348 let errors = GlobalSettingsBuilder::new()
349 .with_local_ip("invalid")
350 .build()
351 .unwrap_err();
352
353 assert_eq!(errors.len(), 1);
354 assert_eq!(
355 errors.first().unwrap().to_string(),
356 "global_settings.local_ip: invalid IP address syntax"
357 );
358 }
359
360 #[test]
361 fn global_settings_builder_should_fails_and_returns_multiple_errors_if_multiple_fields_are_invalid(
362 ) {
363 let errors = GlobalSettingsBuilder::new()
364 .with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421", "//10.42.153.10/tcp/43111"])
365 .with_local_ip("invalid")
366 .build()
367 .unwrap_err();
368
369 assert_eq!(errors.len(), 3);
370 assert_eq!(
371 errors.first().unwrap().to_string(),
372 "global_settings.bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
373 );
374 assert_eq!(
375 errors.get(1).unwrap().to_string(),
376 "global_settings.bootnodes_addresses[1]: '//10.42.153.10/tcp/43111' unknown protocol string: "
377 );
378 assert_eq!(
379 errors.get(2).unwrap().to_string(),
380 "global_settings.local_ip: invalid IP address syntax"
381 );
382 }
383}