openzeppelin_relayer/config/config_file/network/
collection.rs

1//! Network Configuration Collection Management
2//!
3//! This module provides collection management for multiple network configurations with
4//! inheritance resolution, validation, and flexible loading from JSON arrays or directories.
5//!
6//! ## Core Features
7//!
8//! - **Multi-network support**: Manages EVM, Solana, and Stellar networks in a single collection
9//! - **Inheritance resolution**: Resolves complex inheritance hierarchies with type safety
10//! - **Flexible loading**: Supports JSON arrays and directory-based configuration sources
11//! - **Validation**: Comprehensive validation with detailed error reporting
12
13use super::{InheritanceResolver, NetworkFileConfig, NetworkFileLoader, NetworksSource};
14use crate::config::config_file::ConfigFileNetworkType;
15use crate::config::ConfigFileError;
16use serde::de::{self, Deserializer};
17use serde::{Deserialize, Serialize};
18use std::collections::HashMap;
19use std::ops::Index;
20
21/// Represents the complete configuration for all defined networks.
22///
23/// This structure holds configurations loaded from a file or a directory of files
24/// and provides methods to validate and process them, including resolving inheritance.
25#[derive(Debug, Default, Serialize, Clone)]
26pub struct NetworksFileConfig {
27    pub networks: Vec<NetworkFileConfig>,
28    #[serde(skip)]
29    network_map: HashMap<(ConfigFileNetworkType, String), usize>,
30}
31
32/// Custom deserialization logic for `NetworksFileConfig`.
33///
34/// This allows `NetworksFileConfig` to be created from either a direct list of network
35/// configurations, a path string pointing to a directory of configuration files, or null/missing
36/// for the default path ("./config/networks").
37impl<'de> Deserialize<'de> for NetworksFileConfig {
38    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
39    where
40        D: Deserializer<'de>,
41    {
42        // Use Option to handle missing fields gracefully
43        let source_option: Option<NetworksSource> = Option::deserialize(deserializer)?;
44        let source = source_option.unwrap_or_default();
45
46        let final_networks =
47            NetworkFileLoader::load_from_source(source).map_err(de::Error::custom)?;
48
49        // Check if networks is empty and return error
50        if final_networks.is_empty() {
51            return Err(de::Error::custom(
52                "NetworksFileConfig cannot be empty - networks must contain at least one network configuration"
53            ));
54        }
55
56        // First, create an instance with unflattened networks.
57        // This will perform initial validations like duplicate name checks.
58        let unflattened_config = NetworksFileConfig::new(final_networks).map_err(|e| {
59            de::Error::custom(format!("Error creating initial NetworksFileConfig: {e:?}"))
60        })?;
61
62        // Now, flatten the configuration. (Resolve inheritance)
63        unflattened_config
64            .flatten()
65            .map_err(|e| de::Error::custom(format!("Error flattening NetworksFileConfig: {e:?}")))
66    }
67}
68
69impl NetworksFileConfig {
70    /// Creates a new `NetworksFileConfig` instance from a vector of network configurations.
71    ///
72    /// # Returns
73    /// - `Ok(Self)` if all network names are unique within their respective types and the instance is successfully created.
74    /// - `Err(ConfigFileError)` if duplicate network names are found within the same network type.
75    pub fn new(networks: Vec<NetworkFileConfig>) -> Result<Self, ConfigFileError> {
76        let mut network_map = HashMap::new();
77
78        // Build the network map for efficient lookups
79        for (index, network) in networks.iter().enumerate() {
80            let name = network.network_name();
81            let network_type = network.network_type();
82            let key = (network_type, name.to_string());
83
84            if network_map.insert(key, index).is_some() {
85                // Return an error if we find a duplicate within the same network type
86                return Err(ConfigFileError::DuplicateId(format!(
87                    "{network_type:?} network '{name}'"
88                )));
89            }
90        }
91
92        let instance = Self {
93            networks,
94            network_map,
95        };
96
97        // Check inheritance references and types
98        for network in &instance.networks {
99            if network.inherits_from().is_some() {
100                instance.trace_inheritance(network.network_name(), network.network_type())?;
101            }
102        }
103
104        Ok(instance)
105    }
106
107    /// Retrieves a network configuration by its network type and name.
108    ///
109    /// # Arguments
110    /// * `network_type` - The type of the network to retrieve.
111    /// * `name` - The name of the network to retrieve.
112    ///
113    /// # Returns
114    /// - `Some(&NetworkFileConfig)` if a network with the given type and name exists.
115    /// - `None` if no network with the given type and name is found.
116    pub fn get_network(
117        &self,
118        network_type: ConfigFileNetworkType,
119        name: &str,
120    ) -> Option<&NetworkFileConfig> {
121        let key = (network_type, name.to_string());
122        self.network_map
123            .get(&key)
124            .map(|&index| &self.networks[index])
125    }
126
127    /// Builds a new set of networks with all inheritance chains resolved and flattened.
128    ///
129    /// This method processes all networks and their inheritance relationships to produce
130    /// a set of fully expanded network configurations where each network includes all properties
131    /// from its parent networks, with any overrides applied.
132    ///
133    /// # Returns
134    /// - `Result<NetworksFileConfig, ConfigFileError>` containing either the flattened configuration
135    ///   or an error if any inheritance issues are encountered.
136    pub fn flatten(&self) -> Result<NetworksFileConfig, ConfigFileError> {
137        // Process each network to resolve inheritance
138        let resolved_networks = self
139            .networks
140            .iter()
141            .map(|network| self.resolve_inheritance(network))
142            .collect::<Result<Vec<NetworkFileConfig>, ConfigFileError>>()?;
143
144        NetworksFileConfig::new(resolved_networks)
145    }
146
147    /// Creates a fully resolved network configuration by merging properties from its inheritance chain.
148    ///
149    /// # Arguments
150    /// * `network` - A reference to the `NetworkFileConfig` to resolve.
151    ///
152    /// # Returns
153    /// - `Ok(NetworkFileConfig)` containing the fully resolved network configuration.
154    /// - `Err(ConfigFileError)` if any issues are encountered during inheritance resolution.
155    fn resolve_inheritance(
156        &self,
157        network: &NetworkFileConfig,
158    ) -> Result<NetworkFileConfig, ConfigFileError> {
159        // If no inheritance, return a clone of the original
160        if network.inherits_from().is_none() {
161            return Ok(network.clone());
162        }
163
164        let parent_name = network.inherits_from().unwrap();
165        let network_name = network.network_name();
166        let network_type = network.network_type();
167
168        // Create the inheritance resolver with a lookup function that uses network type
169        let lookup_fn = move |name: &str| self.get_network(network_type, name);
170        let resolver = InheritanceResolver::new(&lookup_fn);
171
172        match network {
173            NetworkFileConfig::Evm(config) => {
174                let resolved_config =
175                    resolver.resolve_evm_inheritance(config, network_name, parent_name)?;
176                Ok(NetworkFileConfig::Evm(resolved_config))
177            }
178            NetworkFileConfig::Solana(config) => {
179                let resolved_config =
180                    resolver.resolve_solana_inheritance(config, network_name, parent_name)?;
181                Ok(NetworkFileConfig::Solana(resolved_config))
182            }
183            NetworkFileConfig::Stellar(config) => {
184                let resolved_config =
185                    resolver.resolve_stellar_inheritance(config, network_name, parent_name)?;
186                Ok(NetworkFileConfig::Stellar(resolved_config))
187            }
188        }
189    }
190
191    /// Validates the entire networks configuration structure.
192    ///
193    /// # Returns
194    /// - `Ok(())` if the entire configuration is valid.
195    /// - `Err(ConfigFileError)` if any validation fails (duplicate names, invalid inheritance,
196    ///   incompatible inheritance types, or errors from individual network validations).
197    pub fn validate(&self) -> Result<(), ConfigFileError> {
198        for network in &self.networks {
199            network.validate()?;
200        }
201        Ok(())
202    }
203
204    /// Traces the inheritance path for a given network to check for cycles or invalid references.
205    ///
206    /// # Arguments
207    /// - `start_network_name` - The name of the network to trace inheritance for.
208    /// - `network_type` - The type of the network to trace inheritance for.
209    ///
210    /// # Returns
211    /// - `Ok(())` if the inheritance chain is valid.
212    /// - `Err(ConfigFileError)` if a cycle or invalid reference is detected.
213    fn trace_inheritance(
214        &self,
215        start_network_name: &str,
216        network_type: ConfigFileNetworkType,
217    ) -> Result<(), ConfigFileError> {
218        let mut current_path_names = Vec::new();
219        let mut current_name = start_network_name;
220
221        loop {
222            // Check cycle first
223            if current_path_names.contains(&current_name) {
224                let cycle_path_str = current_path_names.join(" -> ");
225                return Err(ConfigFileError::CircularInheritance(format!(
226                    "Circular inheritance detected: {cycle_path_str} -> {current_name}"
227                )));
228            }
229
230            current_path_names.push(current_name);
231
232            let current_network =
233                self.get_network(network_type, current_name)
234                    .ok_or_else(|| {
235                        ConfigFileError::InvalidReference(format!(
236                            "{network_type:?} network '{current_name}' not found in configuration"
237                        ))
238                    })?;
239
240            if let Some(source_name) = current_network.inherits_from() {
241                let derived_type = current_network.network_type();
242
243                if source_name == current_name {
244                    return Err(ConfigFileError::InvalidReference(format!(
245                        "Network '{current_name}' cannot inherit from itself"
246                    )));
247                }
248
249                let source_network =
250                    self.get_network(network_type, source_name).ok_or_else(|| {
251                        ConfigFileError::InvalidReference(format!(
252                            "{network_type:?} network '{current_name}' inherits from non-existent network '{source_name}'"
253                        ))
254                    })?;
255
256                let source_type = source_network.network_type();
257
258                if derived_type != source_type {
259                    return Err(ConfigFileError::IncompatibleInheritanceType(format!(
260                        "Network '{current_name}' (type {derived_type:?}) tries to inherit from '{source_name}' (type {source_type:?})"
261                    )));
262                }
263                current_name = source_name;
264            } else {
265                break;
266            }
267        }
268
269        Ok(())
270    }
271
272    /// Returns an iterator over all networks.
273    pub fn iter(&self) -> impl Iterator<Item = &NetworkFileConfig> {
274        self.networks.iter()
275    }
276
277    /// Returns the number of networks in the configuration.
278    pub fn len(&self) -> usize {
279        self.networks.len()
280    }
281
282    /// Returns true if there are no networks in the configuration.
283    pub fn is_empty(&self) -> bool {
284        self.networks.is_empty()
285    }
286
287    /// Filters networks by type.
288    pub fn networks_by_type(
289        &self,
290        network_type: crate::config::config_file::ConfigFileNetworkType,
291    ) -> impl Iterator<Item = &NetworkFileConfig> {
292        self.networks
293            .iter()
294            .filter(move |network| network.network_type() == network_type)
295    }
296
297    /// Gets all network names.
298    pub fn network_names(&self) -> impl Iterator<Item = &str> {
299        self.networks.iter().map(|network| network.network_name())
300    }
301
302    /// Returns the first network in the configuration.
303    ///
304    /// # Returns
305    /// - `Some(&NetworkFileConfig)` if there is at least one network.
306    /// - `None` if the configuration is empty.
307    pub fn first(&self) -> Option<&NetworkFileConfig> {
308        self.networks.first()
309    }
310
311    /// Returns a reference to the network at the given index.
312    ///
313    /// # Arguments
314    /// * `index` - The index of the network to retrieve.
315    ///
316    /// # Returns
317    /// - `Some(&NetworkFileConfig)` if a network exists at the given index.
318    /// - `None` if the index is out of bounds.
319    pub fn get(&self, index: usize) -> Option<&NetworkFileConfig> {
320        self.networks.get(index)
321    }
322}
323
324// Implementation of Index trait for array-like access (config[0])
325impl Index<usize> for NetworksFileConfig {
326    type Output = NetworkFileConfig;
327
328    fn index(&self, index: usize) -> &Self::Output {
329        &self.networks[index]
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336    use crate::config::config_file::network::test_utils::*;
337    use crate::config::config_file::ConfigFileNetworkType;
338    use std::fs::File;
339    use std::io::Write;
340    use tempfile::tempdir;
341
342    #[test]
343    fn test_new_with_single_network() {
344        let networks = vec![create_evm_network_wrapped("test-evm")];
345        let config = NetworksFileConfig::new(networks);
346
347        assert!(config.is_ok());
348        let config = config.unwrap();
349        assert_eq!(config.networks.len(), 1);
350        assert_eq!(config.network_map.len(), 1);
351        assert!(config
352            .network_map
353            .contains_key(&(ConfigFileNetworkType::Evm, "test-evm".to_string())));
354    }
355
356    #[test]
357    fn test_new_with_multiple_networks() {
358        let networks = vec![
359            create_evm_network_wrapped("evm-1"),
360            create_solana_network_wrapped("solana-1"),
361            create_stellar_network_wrapped("stellar-1"),
362        ];
363        let config = NetworksFileConfig::new(networks);
364
365        assert!(config.is_ok());
366        let config = config.unwrap();
367        assert_eq!(config.networks.len(), 3);
368        assert_eq!(config.network_map.len(), 3);
369        assert!(config
370            .network_map
371            .contains_key(&(ConfigFileNetworkType::Evm, "evm-1".to_string())));
372        assert!(config
373            .network_map
374            .contains_key(&(ConfigFileNetworkType::Solana, "solana-1".to_string())));
375        assert!(config
376            .network_map
377            .contains_key(&(ConfigFileNetworkType::Stellar, "stellar-1".to_string())));
378    }
379
380    #[test]
381    fn test_new_with_empty_networks() {
382        let networks = vec![];
383        let config = NetworksFileConfig::new(networks);
384
385        assert!(config.is_ok());
386        let config = config.unwrap();
387        assert_eq!(config.networks.len(), 0);
388        assert_eq!(config.network_map.len(), 0);
389    }
390
391    #[test]
392    fn test_new_with_valid_inheritance() {
393        let networks = vec![
394            create_evm_network_wrapped("parent"),
395            create_evm_network_wrapped_with_parent("child", "parent"),
396        ];
397        let config = NetworksFileConfig::new(networks);
398
399        assert!(config.is_ok());
400        let config = config.unwrap();
401        assert_eq!(config.networks.len(), 2);
402    }
403
404    #[test]
405    fn test_new_with_invalid_inheritance_reference() {
406        let networks = vec![create_evm_network_wrapped_with_parent(
407            "child",
408            "non-existent",
409        )];
410        let result = NetworksFileConfig::new(networks);
411
412        assert!(result.is_err());
413        assert!(matches!(
414            result.unwrap_err(),
415            ConfigFileError::InvalidReference(_)
416        ));
417    }
418
419    #[test]
420    fn test_new_with_self_inheritance() {
421        let networks = vec![create_evm_network_wrapped_with_parent(
422            "self-ref", "self-ref",
423        )];
424        let result = NetworksFileConfig::new(networks);
425
426        assert!(result.is_err());
427        assert!(matches!(
428            result.unwrap_err(),
429            ConfigFileError::InvalidReference(_)
430        ));
431    }
432
433    #[test]
434    fn test_new_with_circular_inheritance() {
435        let networks = vec![
436            create_evm_network_wrapped_with_parent("a", "b"),
437            create_evm_network_wrapped_with_parent("b", "c"),
438            create_evm_network_wrapped_with_parent("c", "a"),
439        ];
440        let result = NetworksFileConfig::new(networks);
441
442        assert!(result.is_err());
443        assert!(matches!(
444            result.unwrap_err(),
445            ConfigFileError::CircularInheritance(_)
446        ));
447    }
448
449    #[test]
450    fn test_new_with_incompatible_inheritance_types() {
451        let networks = vec![
452            create_evm_network_wrapped("evm-parent"),
453            create_solana_network_wrapped_with_parent("solana-child", "evm-parent"),
454        ];
455        let result = NetworksFileConfig::new(networks);
456
457        assert!(result.is_err());
458        let error = result.unwrap_err();
459        assert!(matches!(error, ConfigFileError::InvalidReference(_)));
460    }
461
462    #[test]
463    fn test_new_with_deep_inheritance_chain() {
464        let networks = vec![
465            create_evm_network_wrapped("root"),
466            create_evm_network_wrapped_with_parent("level1", "root"),
467            create_evm_network_wrapped_with_parent("level2", "level1"),
468            create_evm_network_wrapped_with_parent("level3", "level2"),
469        ];
470        let config = NetworksFileConfig::new(networks);
471
472        assert!(config.is_ok());
473        let config = config.unwrap();
474        assert_eq!(config.networks.len(), 4);
475    }
476
477    #[test]
478    fn test_get_network_existing() {
479        let networks = vec![
480            create_evm_network_wrapped("test-evm"),
481            create_solana_network_wrapped("test-solana"),
482        ];
483        let config = NetworksFileConfig::new(networks).unwrap();
484
485        let network = config.get_network(ConfigFileNetworkType::Evm, "test-evm");
486        assert!(network.is_some());
487        assert_eq!(network.unwrap().network_name(), "test-evm");
488
489        let network = config.get_network(ConfigFileNetworkType::Solana, "test-solana");
490        assert!(network.is_some());
491        assert_eq!(network.unwrap().network_name(), "test-solana");
492    }
493
494    #[test]
495    fn test_get_network_non_existent() {
496        let networks = vec![create_evm_network_wrapped("test-evm")];
497        let config = NetworksFileConfig::new(networks).unwrap();
498
499        let network = config.get_network(ConfigFileNetworkType::Evm, "non-existent");
500        assert!(network.is_none());
501    }
502
503    #[test]
504    fn test_get_network_empty_config() {
505        let config = NetworksFileConfig::new(vec![]).unwrap();
506
507        let network = config.get_network(ConfigFileNetworkType::Evm, "any-name");
508        assert!(network.is_none());
509    }
510
511    #[test]
512    fn test_get_network_case_sensitive() {
513        let networks = vec![create_evm_network_wrapped("Test-Network")];
514        let config = NetworksFileConfig::new(networks).unwrap();
515
516        assert!(config
517            .get_network(ConfigFileNetworkType::Evm, "Test-Network")
518            .is_some());
519        assert!(config
520            .get_network(ConfigFileNetworkType::Evm, "test-network")
521            .is_none());
522        assert!(config
523            .get_network(ConfigFileNetworkType::Evm, "TEST-NETWORK")
524            .is_none());
525    }
526
527    #[test]
528    fn test_validate_success() {
529        let networks = vec![
530            create_evm_network_wrapped("evm-1"),
531            create_solana_network_wrapped("solana-1"),
532        ];
533        let config = NetworksFileConfig::new(networks).unwrap();
534
535        let result = config.validate();
536        assert!(result.is_ok());
537    }
538
539    #[test]
540    fn test_validate_with_invalid_network() {
541        let networks = vec![
542            create_evm_network_wrapped("valid"),
543            create_invalid_evm_network_wrapped("invalid"),
544        ];
545        let config = NetworksFileConfig::new(networks).unwrap();
546
547        let result = config.validate();
548        assert!(result.is_err());
549        assert!(matches!(
550            result.unwrap_err(),
551            ConfigFileError::MissingField(_)
552        ));
553    }
554
555    #[test]
556    fn test_validate_empty_config() {
557        let config = NetworksFileConfig::new(vec![]).unwrap();
558
559        let result = config.validate();
560        assert!(result.is_ok()); // Empty config is valid for validation
561    }
562
563    #[test]
564    fn test_flatten_without_inheritance() {
565        let networks = vec![
566            create_evm_network_wrapped("evm-1"),
567            create_solana_network_wrapped("solana-1"),
568        ];
569        let config = NetworksFileConfig::new(networks).unwrap();
570
571        let flattened = config.flatten();
572        assert!(flattened.is_ok());
573        let flattened = flattened.unwrap();
574        assert_eq!(flattened.networks.len(), 2);
575    }
576
577    #[test]
578    fn test_flatten_with_simple_inheritance() {
579        let networks = vec![
580            create_evm_network_wrapped("parent"),
581            create_evm_network_wrapped_with_parent("child", "parent"),
582        ];
583        let config = NetworksFileConfig::new(networks).unwrap();
584
585        let flattened = config.flatten();
586        assert!(flattened.is_ok());
587        let flattened = flattened.unwrap();
588        assert_eq!(flattened.networks.len(), 2);
589
590        // Child should still exist with inheritance information preserved
591        let child = flattened.get_network(ConfigFileNetworkType::Evm, "child");
592        assert!(child.is_some());
593        // The from field is preserved to show inheritance source, but inheritance is resolved
594        assert_eq!(child.unwrap().inherits_from(), Some("parent"));
595    }
596
597    #[test]
598    fn test_flatten_with_multi_level_inheritance() {
599        let networks = vec![
600            create_evm_network_wrapped("root"),
601            create_evm_network_wrapped_with_parent("middle", "root"),
602            create_evm_network_wrapped_with_parent("leaf", "middle"),
603        ];
604        let config = NetworksFileConfig::new(networks).unwrap();
605
606        let flattened = config.flatten();
607        assert!(flattened.is_ok());
608        let flattened = flattened.unwrap();
609        assert_eq!(flattened.networks.len(), 3);
610    }
611
612    #[test]
613    fn test_validation_after_flatten_with_failure() {
614        let networks = vec![
615            create_evm_network_wrapped("valid"),
616            create_invalid_evm_network_wrapped("invalid"),
617        ];
618        let config = NetworksFileConfig::new(networks).unwrap();
619
620        let flattened = config.flatten();
621        assert!(flattened.is_ok());
622        let flattened = flattened.unwrap();
623
624        let result = flattened.validate();
625
626        assert!(result.is_err());
627        assert!(matches!(
628            result.unwrap_err(),
629            ConfigFileError::MissingField(_)
630        ));
631    }
632
633    #[test]
634    fn test_flatten_with_mixed_network_types() {
635        let networks = vec![
636            create_evm_network_wrapped("evm-parent"),
637            create_evm_network_wrapped_with_parent("evm-child", "evm-parent"),
638            create_solana_network_wrapped("solana-parent"),
639            create_solana_network_wrapped_with_parent("solana-child", "solana-parent"),
640            create_stellar_network_wrapped("stellar-standalone"),
641        ];
642        let config = NetworksFileConfig::new(networks).unwrap();
643
644        let flattened = config.flatten();
645        assert!(flattened.is_ok());
646        let flattened = flattened.unwrap();
647        assert_eq!(flattened.networks.len(), 5);
648    }
649
650    #[test]
651    fn test_iter() {
652        let networks = vec![
653            create_evm_network_wrapped("evm-1"),
654            create_solana_network_wrapped("solana-1"),
655        ];
656        let config = NetworksFileConfig::new(networks).unwrap();
657
658        let collected: Vec<_> = config.iter().collect();
659        assert_eq!(collected.len(), 2);
660        assert_eq!(collected[0].network_name(), "evm-1");
661        assert_eq!(collected[1].network_name(), "solana-1");
662    }
663
664    #[test]
665    fn test_len() {
666        let config = NetworksFileConfig::new(vec![]).unwrap();
667        assert_eq!(config.len(), 0);
668
669        let networks = vec![create_evm_network_wrapped("test")];
670        let config = NetworksFileConfig::new(networks).unwrap();
671        assert_eq!(config.len(), 1);
672
673        let networks = vec![
674            create_evm_network_wrapped("test1"),
675            create_solana_network_wrapped("test2"),
676            create_stellar_network_wrapped("test3"),
677        ];
678        let config = NetworksFileConfig::new(networks).unwrap();
679        assert_eq!(config.len(), 3);
680    }
681
682    #[test]
683    fn test_is_empty() {
684        let config = NetworksFileConfig::new(vec![]).unwrap();
685        assert!(config.is_empty());
686
687        let networks = vec![create_evm_network_wrapped("test")];
688        let config = NetworksFileConfig::new(networks).unwrap();
689        assert!(!config.is_empty());
690    }
691
692    #[test]
693    fn test_networks_by_type() {
694        let networks = vec![
695            create_evm_network_wrapped("evm-1"),
696            create_evm_network_wrapped("evm-2"),
697            create_solana_network_wrapped("solana-1"),
698            create_stellar_network_wrapped("stellar-1"),
699        ];
700        let config = NetworksFileConfig::new(networks).unwrap();
701
702        let evm_networks: Vec<_> = config
703            .networks_by_type(ConfigFileNetworkType::Evm)
704            .collect();
705        assert_eq!(evm_networks.len(), 2);
706
707        let solana_networks: Vec<_> = config
708            .networks_by_type(ConfigFileNetworkType::Solana)
709            .collect();
710        assert_eq!(solana_networks.len(), 1);
711
712        let stellar_networks: Vec<_> = config
713            .networks_by_type(ConfigFileNetworkType::Stellar)
714            .collect();
715        assert_eq!(stellar_networks.len(), 1);
716    }
717
718    #[test]
719    fn test_networks_by_type_empty_result() {
720        let networks = vec![create_evm_network_wrapped("evm-only")];
721        let config = NetworksFileConfig::new(networks).unwrap();
722
723        let solana_networks: Vec<_> = config
724            .networks_by_type(ConfigFileNetworkType::Solana)
725            .collect();
726        assert_eq!(solana_networks.len(), 0);
727    }
728
729    #[test]
730    fn test_network_names() {
731        let networks = vec![
732            create_evm_network_wrapped("alpha"),
733            create_solana_network_wrapped("beta"),
734            create_stellar_network_wrapped("gamma"),
735        ];
736        let config = NetworksFileConfig::new(networks).unwrap();
737
738        let names: Vec<_> = config.network_names().collect();
739        assert_eq!(names.len(), 3);
740        assert!(names.contains(&"alpha"));
741        assert!(names.contains(&"beta"));
742        assert!(names.contains(&"gamma"));
743    }
744
745    #[test]
746    fn test_network_names_empty() {
747        let config = NetworksFileConfig::new(vec![]).unwrap();
748
749        let names: Vec<_> = config.network_names().collect();
750        assert_eq!(names.len(), 0);
751    }
752
753    // Tests for Default implementation
754    #[test]
755    fn test_default() {
756        let config = NetworksFileConfig::default();
757
758        assert_eq!(config.networks.len(), 0);
759        assert_eq!(config.network_map.len(), 0);
760        assert!(config.is_empty());
761    }
762
763    #[test]
764    fn test_deserialize_from_array() {
765        let json = r#"[
766            {
767                "type": "evm",
768                "network": "test-evm",
769                "chain_id": 31337,
770                "required_confirmations": 1,
771                "symbol": "ETH",
772                "rpc_urls": ["https://rpc.test.example.com"]
773            }
774        ]"#;
775
776        let result: Result<NetworksFileConfig, _> = serde_json::from_str(json);
777        assert!(result.is_ok());
778        let config = result.unwrap();
779        assert_eq!(config.len(), 1);
780        assert!(config
781            .get_network(ConfigFileNetworkType::Evm, "test-evm")
782            .is_some());
783    }
784
785    #[test]
786    fn test_deserialize_empty_array_returns_error() {
787        let json = r#"[]"#;
788        let result: Result<NetworksFileConfig, _> = serde_json::from_str(json);
789
790        assert!(result.is_err());
791        let error_message = result.unwrap_err().to_string();
792        assert!(error_message.contains("NetworksFileConfig cannot be empty"));
793    }
794
795    #[test]
796    fn test_deserialize_from_directory() {
797        let dir = tempdir().expect("Failed to create temp dir");
798        let network_dir_path = dir.path();
799
800        // Create test network files
801        let evm_file = network_dir_path.join("evm.json");
802        let mut file = File::create(&evm_file).expect("Failed to create EVM file");
803        writeln!(file, r#"{{"networks": [{{"type": "evm", "network": "test-evm-from-file", "chain_id": 31337, "required_confirmations": 1, "symbol": "ETH", "rpc_urls": ["https://rpc.test.example.com"]}}]}}"#).expect("Failed to write EVM file");
804
805        let solana_file = network_dir_path.join("solana.json");
806        let mut file = File::create(&solana_file).expect("Failed to create Solana file");
807        writeln!(file, r#"{{"networks": [{{"type": "solana", "network": "test-solana-from-file", "rpc_urls": ["https://rpc.solana.example.com"]}}]}}"#).expect("Failed to write Solana file");
808
809        let json = format!(r#""{}""#, network_dir_path.to_str().unwrap());
810
811        let result: Result<NetworksFileConfig, _> = serde_json::from_str(&json);
812        assert!(result.is_ok());
813        let config = result.unwrap();
814        assert_eq!(config.len(), 2);
815        assert!(config
816            .get_network(ConfigFileNetworkType::Evm, "test-evm-from-file")
817            .is_some());
818        assert!(config
819            .get_network(ConfigFileNetworkType::Solana, "test-solana-from-file")
820            .is_some());
821    }
822
823    #[test]
824    fn test_deserialize_invalid_directory() {
825        let json = r#""/non/existent/directory""#;
826
827        let result: Result<NetworksFileConfig, _> = serde_json::from_str(json);
828        assert!(result.is_err());
829    }
830
831    #[test]
832    fn test_deserialize_networks_array() {
833        let json = r#"[
834            {
835                "type": "evm",
836                "network": "testnet",
837                "chain_id": 11155111,
838                "required_confirmations": 6,
839                "symbol": "ETH",
840                "rpc_urls": ["https://sepolia.drpc.org", "https://1rpc.io/sepolia"]
841            },
842            {
843                "type": "solana",
844                "network": "devnet",
845                "rpc_urls": ["https://api.devnet.solana.com"],
846                "explorer_urls": ["https://explorer.solana.com?cluster=devnet"],
847                "average_blocktime_ms": 400,
848                "is_testnet": true
849            }
850        ]"#;
851
852        let result: Result<NetworksFileConfig, _> = serde_json::from_str(json);
853        assert!(result.is_ok());
854        let config = result.unwrap();
855        assert_eq!(config.len(), 2);
856
857        // Verify EVM network
858        let evm_network = config
859            .get_network(ConfigFileNetworkType::Evm, "testnet")
860            .unwrap();
861        if let NetworkFileConfig::Evm(evm_config) = evm_network {
862            assert_eq!(evm_config.chain_id, Some(11155111));
863            assert!(evm_config.common.rpc_urls.is_some());
864            assert_eq!(evm_config.common.rpc_urls.as_ref().unwrap().len(), 2);
865        }
866
867        // Verify Solana network
868        let solana_network = config
869            .get_network(ConfigFileNetworkType::Solana, "devnet")
870            .unwrap();
871        if let NetworkFileConfig::Solana(solana_config) = solana_network {
872            assert_eq!(solana_config.common.average_blocktime_ms, Some(400));
873            assert_eq!(solana_config.common.is_testnet, Some(true));
874        }
875    }
876
877    #[test]
878    fn test_deserialize_with_invalid_inheritance() {
879        let json = r#"[
880            {
881                "type": "evm",
882                "network": "child",
883                "from": "non-existent-parent",
884                "chain_id": 31337,
885                "required_confirmations": 1,
886                "symbol": "ETH",
887                "rpc_urls": ["https://rpc.test.example.com"]
888            }
889        ]"#;
890
891        let result: Result<NetworksFileConfig, _> = serde_json::from_str(json);
892        assert!(result.is_err());
893    }
894
895    // Edge cases and stress tests
896    #[test]
897    fn test_large_number_of_networks() {
898        let mut networks = Vec::new();
899        for i in 0..100 {
900            networks.push(create_evm_network_wrapped(&format!("network-{i}")));
901        }
902
903        let config = NetworksFileConfig::new(networks);
904        assert!(config.is_ok());
905        let config = config.unwrap();
906        assert_eq!(config.len(), 100);
907
908        // Test that all networks are accessible
909        for i in 0..100 {
910            assert!(config
911                .get_network(ConfigFileNetworkType::Evm, &format!("network-{i}"))
912                .is_some());
913        }
914    }
915
916    #[test]
917    fn test_unicode_network_names() {
918        let networks = vec![
919            create_evm_network_wrapped("测试网络"),
920            create_solana_network_wrapped("тестовая-сеть"),
921            create_stellar_network_wrapped("réseau-test"),
922        ];
923
924        let config = NetworksFileConfig::new(networks);
925        assert!(config.is_ok());
926        let config = config.unwrap();
927        assert_eq!(config.len(), 3);
928        assert!(config
929            .get_network(ConfigFileNetworkType::Evm, "测试网络")
930            .is_some());
931        assert!(config
932            .get_network(ConfigFileNetworkType::Solana, "тестовая-сеть")
933            .is_some());
934        assert!(config
935            .get_network(ConfigFileNetworkType::Stellar, "réseau-test")
936            .is_some());
937    }
938
939    #[test]
940    fn test_special_characters_in_network_names() {
941        let networks = vec![
942            create_evm_network_wrapped("test-network_123"),
943            create_solana_network_wrapped("test.network.with.dots"),
944            create_stellar_network_wrapped("test@network#with$symbols"),
945        ];
946
947        let config = NetworksFileConfig::new(networks);
948        assert!(config.is_ok());
949        let config = config.unwrap();
950        assert_eq!(config.len(), 3);
951    }
952
953    #[test]
954    fn test_very_long_network_names() {
955        let long_name = "a".repeat(1000);
956        let networks = vec![create_evm_network_wrapped(&long_name)];
957
958        let config = NetworksFileConfig::new(networks);
959        assert!(config.is_ok());
960        let config = config.unwrap();
961        assert!(config
962            .get_network(ConfigFileNetworkType::Evm, &long_name)
963            .is_some());
964    }
965
966    #[test]
967    fn test_complex_inheritance_scenario() {
968        let networks = vec![
969            // Root networks
970            create_evm_network_wrapped("evm-root"),
971            create_solana_network_wrapped("solana-root"),
972            // First level children
973            create_evm_network_wrapped_with_parent("evm-child1", "evm-root"),
974            create_evm_network_wrapped_with_parent("evm-child2", "evm-root"),
975            create_solana_network_wrapped_with_parent("solana-child1", "solana-root"),
976            // Second level children
977            create_evm_network_wrapped_with_parent("evm-grandchild", "evm-child1"),
978        ];
979
980        let config = NetworksFileConfig::new(networks);
981        assert!(config.is_ok());
982        let config = config.unwrap();
983        assert_eq!(config.len(), 6);
984
985        let flattened = config.flatten();
986        assert!(flattened.is_ok());
987        let flattened = flattened.unwrap();
988        assert_eq!(flattened.len(), 6);
989    }
990
991    #[test]
992    fn test_new_with_duplicate_network_names_across_types() {
993        // Should allow same name across different network types
994        let networks = vec![
995            create_evm_network_wrapped("mainnet"),
996            create_solana_network_wrapped("mainnet"),
997            create_stellar_network_wrapped("mainnet"),
998        ];
999        let result = NetworksFileConfig::new(networks);
1000
1001        assert!(result.is_ok());
1002        let config = result.unwrap();
1003        assert_eq!(config.networks.len(), 3);
1004        assert_eq!(config.network_map.len(), 3);
1005
1006        // Verify we can retrieve each network by type and name
1007        assert!(config
1008            .get_network(ConfigFileNetworkType::Evm, "mainnet")
1009            .is_some());
1010        assert!(config
1011            .get_network(ConfigFileNetworkType::Solana, "mainnet")
1012            .is_some());
1013        assert!(config
1014            .get_network(ConfigFileNetworkType::Stellar, "mainnet")
1015            .is_some());
1016    }
1017
1018    #[test]
1019    fn test_new_with_duplicate_network_names_within_same_type() {
1020        let networks = vec![
1021            create_evm_network_wrapped("duplicate-evm"),
1022            create_evm_network_wrapped("duplicate-evm"),
1023        ];
1024        let result = NetworksFileConfig::new(networks);
1025
1026        assert!(result.is_err());
1027        assert!(matches!(
1028            result.unwrap_err(),
1029            ConfigFileError::DuplicateId(_)
1030        ));
1031    }
1032
1033    #[test]
1034    fn test_get_with_empty_config() {
1035        let config = NetworksFileConfig::new(vec![]).unwrap();
1036
1037        let network_0 = config.get(0);
1038        assert!(network_0.is_none());
1039    }
1040
1041    #[test]
1042    fn test_get_and_first_equivalence() {
1043        let networks = vec![create_evm_network_wrapped("test-network")];
1044        let config = NetworksFileConfig::new(networks).unwrap();
1045
1046        // Both methods should return the same result
1047        let network_via_get = config.get(0);
1048        let network_via_first = config.first();
1049
1050        assert!(network_via_get.is_some());
1051        assert!(network_via_first.is_some());
1052        assert_eq!(
1053            network_via_get.unwrap().network_name(),
1054            network_via_first.unwrap().network_name()
1055        );
1056        assert_eq!(network_via_get.unwrap().network_name(), "test-network");
1057    }
1058
1059    #[test]
1060    #[allow(clippy::get_first)]
1061    fn test_different_access_methods() {
1062        let networks = vec![
1063            create_evm_network_wrapped("network-0"),
1064            create_solana_network_wrapped("network-1"),
1065        ];
1066        let config = NetworksFileConfig::new(networks).unwrap();
1067
1068        // Method 1: Using .get())
1069        let net_0_get = config.get(0);
1070        assert!(net_0_get.is_some());
1071        assert_eq!(net_0_get.unwrap().network_name(), "network-0");
1072
1073        // Method 2: Using .first()
1074        let net_0_first = config.first();
1075        assert!(net_0_first.is_some());
1076        assert_eq!(net_0_first.unwrap().network_name(), "network-0");
1077
1078        // Method 3: Using indexing [0] (Index trait)
1079        let net_0_index = &config[0];
1080        assert_eq!(net_0_index.network_name(), "network-0");
1081
1082        // Method 4: Using direct field access
1083        let net_0_direct = config.networks.get(0);
1084        assert!(net_0_direct.is_some());
1085        assert_eq!(net_0_direct.unwrap().network_name(), "network-0");
1086
1087        // All should reference the same network
1088        assert_eq!(
1089            net_0_get.unwrap().network_name(),
1090            net_0_first.unwrap().network_name()
1091        );
1092        assert_eq!(
1093            net_0_get.unwrap().network_name(),
1094            net_0_index.network_name()
1095        );
1096        assert_eq!(
1097            net_0_get.unwrap().network_name(),
1098            net_0_direct.unwrap().network_name()
1099        );
1100    }
1101
1102    #[test]
1103    fn test_first_with_non_empty_config() {
1104        let networks = vec![
1105            create_evm_network_wrapped("first-network"),
1106            create_solana_network_wrapped("second-network"),
1107        ];
1108        let config = NetworksFileConfig::new(networks).unwrap();
1109
1110        let first_network = config.first();
1111        assert!(first_network.is_some());
1112        assert_eq!(first_network.unwrap().network_name(), "first-network");
1113    }
1114
1115    #[test]
1116    fn test_first_with_empty_config() {
1117        let config = NetworksFileConfig::new(vec![]).unwrap();
1118
1119        let first_network = config.first();
1120        assert!(first_network.is_none());
1121    }
1122
1123    #[test]
1124    fn test_get_with_valid_index() {
1125        let networks = vec![
1126            create_evm_network_wrapped("network-0"),
1127            create_solana_network_wrapped("network-1"),
1128            create_stellar_network_wrapped("network-2"),
1129        ];
1130        let config = NetworksFileConfig::new(networks).unwrap();
1131
1132        let network_0 = config.get(0);
1133        assert!(network_0.is_some());
1134        assert_eq!(network_0.unwrap().network_name(), "network-0");
1135
1136        let network_1 = config.get(1);
1137        assert!(network_1.is_some());
1138        assert_eq!(network_1.unwrap().network_name(), "network-1");
1139
1140        let network_2 = config.get(2);
1141        assert!(network_2.is_some());
1142        assert_eq!(network_2.unwrap().network_name(), "network-2");
1143    }
1144
1145    #[test]
1146    fn test_get_with_invalid_index() {
1147        let networks = vec![create_evm_network_wrapped("only-network")];
1148        let config = NetworksFileConfig::new(networks).unwrap();
1149
1150        let network_out_of_bounds = config.get(1);
1151        assert!(network_out_of_bounds.is_none());
1152
1153        let network_large_index = config.get(100);
1154        assert!(network_large_index.is_none());
1155    }
1156
1157    #[test]
1158    fn test_networks_source_default() {
1159        let default_source = NetworksSource::default();
1160        match default_source {
1161            NetworksSource::Path(path) => {
1162                assert_eq!(path, "./config/networks");
1163            }
1164            _ => panic!("Default should be a Path variant"),
1165        }
1166    }
1167}