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_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}
45
46impl GlobalSettings {
47 pub fn bootnodes_addresses(&self) -> Vec<&Multiaddr> {
49 self.bootnodes_addresses.iter().collect()
50 }
51
52 pub fn network_spawn_timeout(&self) -> Duration {
54 self.network_spawn_timeout
55 }
56
57 pub fn node_spawn_timeout(&self) -> Duration {
59 self.node_spawn_timeout
60 }
61
62 pub fn local_ip(&self) -> Option<&IpAddr> {
64 self.local_ip.as_ref()
65 }
66
67 pub fn base_dir(&self) -> Option<&Path> {
70 self.base_dir.as_deref()
71 }
72
73 pub fn spawn_concurrency(&self) -> Option<usize> {
75 self.spawn_concurrency
76 }
77}
78
79impl Default for GlobalSettings {
80 fn default() -> Self {
81 Self {
82 bootnodes_addresses: Default::default(),
83 network_spawn_timeout: default_timeout(),
84 node_spawn_timeout: default_node_spawn_timeout(),
85 local_ip: Default::default(),
86 base_dir: Default::default(),
87 spawn_concurrency: Default::default(),
88 }
89 }
90}
91
92#[derive(Default)]
94pub struct GlobalSettingsBuilder {
95 config: GlobalSettings,
96 errors: Vec<anyhow::Error>,
97}
98
99impl GlobalSettingsBuilder {
100 pub fn new() -> Self {
101 Self::default()
102 }
103
104 fn transition(config: GlobalSettings, errors: Vec<anyhow::Error>) -> Self {
106 Self { config, errors }
107 }
108
109 pub fn with_bootnodes_addresses<T>(self, bootnodes_addresses: Vec<T>) -> Self
111 where
112 T: TryInto<Multiaddr> + Display + Copy,
113 T::Error: Error + Send + Sync + 'static,
114 {
115 let mut addrs = vec![];
116 let mut errors = vec![];
117
118 for (index, addr) in bootnodes_addresses.into_iter().enumerate() {
119 match addr.try_into() {
120 Ok(addr) => addrs.push(addr),
121 Err(error) => errors.push(
122 FieldError::BootnodesAddress(index, addr.to_string(), error.into()).into(),
123 ),
124 }
125 }
126
127 Self::transition(
128 GlobalSettings {
129 bootnodes_addresses: addrs,
130 ..self.config
131 },
132 merge_errors_vecs(self.errors, errors),
133 )
134 }
135
136 pub fn with_network_spawn_timeout(self, timeout: Duration) -> Self {
138 Self::transition(
139 GlobalSettings {
140 network_spawn_timeout: timeout,
141 ..self.config
142 },
143 self.errors,
144 )
145 }
146
147 pub fn with_node_spawn_timeout(self, timeout: Duration) -> Self {
149 Self::transition(
150 GlobalSettings {
151 node_spawn_timeout: timeout,
152 ..self.config
153 },
154 self.errors,
155 )
156 }
157
158 pub fn with_local_ip(self, local_ip: &str) -> Self {
160 match IpAddr::from_str(local_ip) {
161 Ok(local_ip) => Self::transition(
162 GlobalSettings {
163 local_ip: Some(local_ip),
164 ..self.config
165 },
166 self.errors,
167 ),
168 Err(error) => Self::transition(
169 self.config,
170 merge_errors(self.errors, FieldError::LocalIp(error.into()).into()),
171 ),
172 }
173 }
174
175 pub fn with_base_dir(self, base_dir: impl Into<PathBuf>) -> Self {
177 Self::transition(
178 GlobalSettings {
179 base_dir: Some(base_dir.into()),
180 ..self.config
181 },
182 self.errors,
183 )
184 }
185
186 pub fn with_spawn_concurrency(self, spawn_concurrency: usize) -> Self {
188 Self::transition(
189 GlobalSettings {
190 spawn_concurrency: Some(spawn_concurrency),
191 ..self.config
192 },
193 self.errors,
194 )
195 }
196
197 pub fn build(self) -> Result<GlobalSettings, Vec<anyhow::Error>> {
199 if !self.errors.is_empty() {
200 return Err(self
201 .errors
202 .into_iter()
203 .map(|error| ConfigError::GlobalSettings(error).into())
204 .collect::<Vec<_>>());
205 }
206
207 Ok(self.config)
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[test]
216 fn global_settings_config_builder_should_succeeds_and_returns_a_global_settings_config() {
217 let global_settings_config = GlobalSettingsBuilder::new()
218 .with_bootnodes_addresses(vec![
219 "/ip4/10.41.122.55/tcp/45421",
220 "/ip4/51.144.222.10/tcp/2333",
221 ])
222 .with_network_spawn_timeout(600)
223 .with_node_spawn_timeout(120)
224 .with_local_ip("10.0.0.1")
225 .with_base_dir("/home/nonroot/mynetwork")
226 .with_spawn_concurrency(5)
227 .build()
228 .unwrap();
229
230 let bootnodes_addresses: Vec<Multiaddr> = vec![
231 "/ip4/10.41.122.55/tcp/45421".try_into().unwrap(),
232 "/ip4/51.144.222.10/tcp/2333".try_into().unwrap(),
233 ];
234 assert_eq!(
235 global_settings_config.bootnodes_addresses(),
236 bootnodes_addresses.iter().collect::<Vec<_>>()
237 );
238 assert_eq!(global_settings_config.network_spawn_timeout(), 600);
239 assert_eq!(global_settings_config.node_spawn_timeout(), 120);
240 assert_eq!(
241 global_settings_config
242 .local_ip()
243 .unwrap()
244 .to_string()
245 .as_str(),
246 "10.0.0.1"
247 );
248 assert_eq!(
249 global_settings_config.base_dir().unwrap(),
250 Path::new("/home/nonroot/mynetwork")
251 );
252 assert_eq!(global_settings_config.spawn_concurrency().unwrap(), 5)
253 }
254
255 #[test]
256 fn global_settings_config_builder_should_succeeds_when_node_spawn_timeout_is_missing() {
257 let global_settings_config = GlobalSettingsBuilder::new()
258 .with_bootnodes_addresses(vec![
259 "/ip4/10.41.122.55/tcp/45421",
260 "/ip4/51.144.222.10/tcp/2333",
261 ])
262 .with_network_spawn_timeout(600)
263 .with_local_ip("10.0.0.1")
264 .build()
265 .unwrap();
266
267 let bootnodes_addresses: Vec<Multiaddr> = vec![
268 "/ip4/10.41.122.55/tcp/45421".try_into().unwrap(),
269 "/ip4/51.144.222.10/tcp/2333".try_into().unwrap(),
270 ];
271 assert_eq!(
272 global_settings_config.bootnodes_addresses(),
273 bootnodes_addresses.iter().collect::<Vec<_>>()
274 );
275 assert_eq!(global_settings_config.network_spawn_timeout(), 600);
276 assert_eq!(global_settings_config.node_spawn_timeout(), 600);
277 assert_eq!(
278 global_settings_config
279 .local_ip()
280 .unwrap()
281 .to_string()
282 .as_str(),
283 "10.0.0.1"
284 );
285 }
286
287 #[test]
288 fn global_settings_builder_should_fails_and_returns_an_error_if_one_bootnode_address_is_invalid(
289 ) {
290 let errors = GlobalSettingsBuilder::new()
291 .with_bootnodes_addresses(vec!["/ip4//tcp/45421"])
292 .build()
293 .unwrap_err();
294
295 assert_eq!(errors.len(), 1);
296 assert_eq!(
297 errors.first().unwrap().to_string(),
298 "global_settings.bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
299 );
300 }
301
302 #[test]
303 fn global_settings_builder_should_fails_and_returns_multiple_errors_if_multiple_bootnodes_addresses_are_invalid(
304 ) {
305 let errors = GlobalSettingsBuilder::new()
306 .with_bootnodes_addresses(vec!["/ip4//tcp/45421", "//10.42.153.10/tcp/43111"])
307 .build()
308 .unwrap_err();
309
310 assert_eq!(errors.len(), 2);
311 assert_eq!(
312 errors.first().unwrap().to_string(),
313 "global_settings.bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
314 );
315 assert_eq!(
316 errors.get(1).unwrap().to_string(),
317 "global_settings.bootnodes_addresses[1]: '//10.42.153.10/tcp/43111' unknown protocol string: "
318 );
319 }
320
321 #[test]
322 fn global_settings_builder_should_fails_and_returns_an_error_if_local_ip_is_invalid() {
323 let errors = GlobalSettingsBuilder::new()
324 .with_local_ip("invalid")
325 .build()
326 .unwrap_err();
327
328 assert_eq!(errors.len(), 1);
329 assert_eq!(
330 errors.first().unwrap().to_string(),
331 "global_settings.local_ip: invalid IP address syntax"
332 );
333 }
334
335 #[test]
336 fn global_settings_builder_should_fails_and_returns_multiple_errors_if_multiple_fields_are_invalid(
337 ) {
338 let errors = GlobalSettingsBuilder::new()
339 .with_bootnodes_addresses(vec!["/ip4//tcp/45421", "//10.42.153.10/tcp/43111"])
340 .with_local_ip("invalid")
341 .build()
342 .unwrap_err();
343
344 assert_eq!(errors.len(), 3);
345 assert_eq!(
346 errors.first().unwrap().to_string(),
347 "global_settings.bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
348 );
349 assert_eq!(
350 errors.get(1).unwrap().to_string(),
351 "global_settings.bootnodes_addresses[1]: '//10.42.153.10/tcp/43111' unknown protocol string: "
352 );
353 assert_eq!(
354 errors.get(2).unwrap().to_string(),
355 "global_settings.local_ip: invalid IP address syntax"
356 );
357 }
358}