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!(
60                "Error creating initial NetworksFileConfig: {:?}",
61                e
62            ))
63        })?;
64
65        // Now, flatten the configuration. (Resolve inheritance)
66        unflattened_config
67            .flatten()
68            .map_err(|e| de::Error::custom(format!("Error flattening NetworksFileConfig: {:?}", e)))
69    }
70}
71
72impl NetworksFileConfig {
73    /// Creates a new `NetworksFileConfig` instance from a vector of network configurations.
74    ///
75    /// # Returns
76    /// - `Ok(Self)` if all network names are unique within their respective types and the instance is successfully created.
77    /// - `Err(ConfigFileError)` if duplicate network names are found within the same network type.
78    pub fn new(networks: Vec<NetworkFileConfig>) -> Result<Self, ConfigFileError> {
79        let mut network_map = HashMap::new();
80
81        // Build the network map for efficient lookups
82        for (index, network) in networks.iter().enumerate() {
83            let name = network.network_name();
84            let network_type = network.network_type();
85            let key = (network_type, name.to_string());
86
87            if network_map.insert(key, index).is_some() {
88                // Return an error if we find a duplicate within the same network type
89                return Err(ConfigFileError::DuplicateId(format!(
90                    "{:?} network '{}'",
91                    network_type, name
92                )));
93            }
94        }
95
96        let instance = Self {
97            networks,
98            network_map,
99        };
100
101        // Check inheritance references and types
102        for network in &instance.networks {
103            if network.inherits_from().is_some() {
104                instance.trace_inheritance(network.network_name(), network.network_type())?;
105            }
106        }
107
108        Ok(instance)
109    }
110
111    /// Retrieves a network configuration by its network type and name.
112    ///
113    /// # Arguments
114    /// * `network_type` - The type of the network to retrieve.
115    /// * `name` - The name of the network to retrieve.
116    ///
117    /// # Returns
118    /// - `Some(&NetworkFileConfig)` if a network with the given type and name exists.
119    /// - `None` if no network with the given type and name is found.
120    pub fn get_network(
121        &self,
122        network_type: ConfigFileNetworkType,
123        name: &str,
124    ) -> Option<&NetworkFileConfig> {
125        let key = (network_type, name.to_string());
126        self.network_map
127            .get(&key)
128            .map(|&index| &self.networks[index])
129    }
130
131    /// Builds a new set of networks with all inheritance chains resolved and flattened.
132    ///
133    /// This method processes all networks and their inheritance relationships to produce
134    /// a set of fully expanded network configurations where each network includes all properties
135    /// from its parent networks, with any overrides applied.
136    ///
137    /// # Returns
138    /// - `Result<NetworksFileConfig, ConfigFileError>` containing either the flattened configuration
139    ///   or an error if any inheritance issues are encountered.
140    pub fn flatten(&self) -> Result<NetworksFileConfig, ConfigFileError> {
141        // First validate the individual network configurations.
142        self.validate()?;
143
144        // Process each network to resolve inheritance
145        let resolved_networks = self
146            .networks
147            .iter()
148            .map(|network| self.resolve_inheritance(network))
149            .collect::<Result<Vec<NetworkFileConfig>, ConfigFileError>>()?;
150
151        NetworksFileConfig::new(resolved_networks)
152    }
153
154    /// Creates a fully resolved network configuration by merging properties from its inheritance chain.
155    ///
156    /// # Arguments
157    /// * `network` - A reference to the `NetworkFileConfig` to resolve.
158    ///
159    /// # Returns
160    /// - `Ok(NetworkFileConfig)` containing the fully resolved network configuration.
161    /// - `Err(ConfigFileError)` if any issues are encountered during inheritance resolution.
162    fn resolve_inheritance(
163        &self,
164        network: &NetworkFileConfig,
165    ) -> Result<NetworkFileConfig, ConfigFileError> {
166        // If no inheritance, return a clone of the original
167        if network.inherits_from().is_none() {
168            return Ok(network.clone());
169        }
170
171        let parent_name = network.inherits_from().unwrap();
172        let network_name = network.network_name();
173        let network_type = network.network_type();
174
175        // Create the inheritance resolver with a lookup function that uses network type
176        let lookup_fn = move |name: &str| self.get_network(network_type, name);
177        let resolver = InheritanceResolver::new(&lookup_fn);
178
179        match network {
180            NetworkFileConfig::Evm(config) => {
181                let resolved_config =
182                    resolver.resolve_evm_inheritance(config, network_name, parent_name)?;
183                Ok(NetworkFileConfig::Evm(resolved_config))
184            }
185            NetworkFileConfig::Solana(config) => {
186                let resolved_config =
187                    resolver.resolve_solana_inheritance(config, network_name, parent_name)?;
188                Ok(NetworkFileConfig::Solana(resolved_config))
189            }
190            NetworkFileConfig::Stellar(config) => {
191                let resolved_config =
192                    resolver.resolve_stellar_inheritance(config, network_name, parent_name)?;
193                Ok(NetworkFileConfig::Stellar(resolved_config))
194            }
195        }
196    }
197
198    /// Validates the entire networks configuration structure.
199    ///
200    /// # Returns
201    /// - `Ok(())` if the entire configuration is valid.
202    /// - `Err(ConfigFileError)` if any validation fails (duplicate names, invalid inheritance,
203    ///   incompatible inheritance types, or errors from individual network validations).
204    pub fn validate(&self) -> Result<(), ConfigFileError> {
205        for network in &self.networks {
206            network.validate()?;
207        }
208        Ok(())
209    }
210
211    /// Traces the inheritance path for a given network to check for cycles or invalid references.
212    ///
213    /// # Arguments
214    /// - `start_network_name` - The name of the network to trace inheritance for.
215    /// - `network_type` - The type of the network to trace inheritance for.
216    ///
217    /// # Returns
218    /// - `Ok(())` if the inheritance chain is valid.
219    /// - `Err(ConfigFileError)` if a cycle or invalid reference is detected.
220    fn trace_inheritance(
221        &self,
222        start_network_name: &str,
223        network_type: ConfigFileNetworkType,
224    ) -> Result<(), ConfigFileError> {
225        let mut current_path_names = Vec::new();
226        let mut current_name = start_network_name;
227
228        loop {
229            // Check cycle first
230            if current_path_names.contains(&current_name) {
231                let cycle_path_str = current_path_names.join(" -> ");
232                return Err(ConfigFileError::CircularInheritance(format!(
233                    "Circular inheritance detected: {} -> {}",
234                    cycle_path_str, current_name
235                )));
236            }
237
238            current_path_names.push(current_name);
239
240            let current_network =
241                self.get_network(network_type, current_name)
242                    .ok_or_else(|| {
243                        ConfigFileError::InvalidReference(format!(
244                            "{:?} network '{}' not found in configuration",
245                            network_type, current_name
246                        ))
247                    })?;
248
249            if let Some(source_name) = current_network.inherits_from() {
250                let derived_type = current_network.network_type();
251
252                if source_name == current_name {
253                    return Err(ConfigFileError::InvalidReference(format!(
254                        "Network '{}' cannot inherit from itself",
255                        current_name
256                    )));
257                }
258
259                let source_network =
260                    self.get_network(network_type, source_name).ok_or_else(|| {
261                        ConfigFileError::InvalidReference(format!(
262                            "{:?} network '{}' inherits from non-existent network '{}'",
263                            network_type, current_name, source_name
264                        ))
265                    })?;
266
267                let source_type = source_network.network_type();
268
269                if derived_type != source_type {
270                    return Err(ConfigFileError::IncompatibleInheritanceType(format!(
271                        "Network '{}' (type {:?}) tries to inherit from '{}' (type {:?})",
272                        current_name, derived_type, source_name, source_type
273                    )));
274                }
275                current_name = source_name;
276            } else {
277                break;
278            }
279        }
280
281        Ok(())
282    }
283
284    /// Returns an iterator over all networks.
285    pub fn iter(&self) -> impl Iterator<Item = &NetworkFileConfig> {
286        self.networks.iter()
287    }
288
289    /// Returns the number of networks in the configuration.
290    pub fn len(&self) -> usize {
291        self.networks.len()
292    }
293
294    /// Returns true if there are no networks in the configuration.
295    pub fn is_empty(&self) -> bool {
296        self.networks.is_empty()
297    }
298
299    /// Filters networks by type.
300    pub fn networks_by_type(
301        &self,
302        network_type: crate::config::config_file::ConfigFileNetworkType,
303    ) -> impl Iterator<Item = &NetworkFileConfig> {
304        self.networks
305            .iter()
306            .filter(move |network| network.network_type() == network_type)
307    }
308
309    /// Gets all network names.
310    pub fn network_names(&self) -> impl Iterator<Item = &str> {
311        self.networks.iter().map(|network| network.network_name())
312    }
313
314    /// Returns the first network in the configuration.
315    ///
316    /// # Returns
317    /// - `Some(&NetworkFileConfig)` if there is at least one network.
318    /// - `None` if the configuration is empty.
319    pub fn first(&self) -> Option<&NetworkFileConfig> {
320        self.networks.first()
321    }
322
323    /// Returns a reference to the network at the given index.
324    ///
325    /// # Arguments
326    /// * `index` - The index of the network to retrieve.
327    ///
328    /// # Returns
329    /// - `Some(&NetworkFileConfig)` if a network exists at the given index.
330    /// - `None` if the index is out of bounds.
331    pub fn get(&self, index: usize) -> Option<&NetworkFileConfig> {
332        self.networks.get(index)
333    }
334}
335
336// Implementation of Index trait for array-like access (config[0])
337impl Index<usize> for NetworksFileConfig {
338    type Output = NetworkFileConfig;
339
340    fn index(&self, index: usize) -> &Self::Output {
341        &self.networks[index]
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348    use crate::config::config_file::network::test_utils::*;
349    use crate::config::config_file::ConfigFileNetworkType;
350    use std::fs::File;
351    use std::io::Write;
352    use tempfile::tempdir;
353
354    #[test]
355    fn test_new_with_single_network() {
356        let networks = vec![create_evm_network_wrapped("test-evm")];
357        let config = NetworksFileConfig::new(networks);
358
359        assert!(config.is_ok());
360        let config = config.unwrap();
361        assert_eq!(config.networks.len(), 1);
362        assert_eq!(config.network_map.len(), 1);
363        assert!(config
364            .network_map
365            .contains_key(&(ConfigFileNetworkType::Evm, "test-evm".to_string())));
366    }
367
368    #[test]
369    fn test_new_with_multiple_networks() {
370        let networks = vec![
371            create_evm_network_wrapped("evm-1"),
372            create_solana_network_wrapped("solana-1"),
373            create_stellar_network_wrapped("stellar-1"),
374        ];
375        let config = NetworksFileConfig::new(networks);
376
377        assert!(config.is_ok());
378        let config = config.unwrap();
379        assert_eq!(config.networks.len(), 3);
380        assert_eq!(config.network_map.len(), 3);
381        assert!(config
382            .network_map
383            .contains_key(&(ConfigFileNetworkType::Evm, "evm-1".to_string())));
384        assert!(config
385            .network_map
386            .contains_key(&(ConfigFileNetworkType::Solana, "solana-1".to_string())));
387        assert!(config
388            .network_map
389            .contains_key(&(ConfigFileNetworkType::Stellar, "stellar-1".to_string())));
390    }
391
392    #[test]
393    fn test_new_with_empty_networks() {
394        let networks = vec![];
395        let config = NetworksFileConfig::new(networks);
396
397        assert!(config.is_ok());
398        let config = config.unwrap();
399        assert_eq!(config.networks.len(), 0);
400        assert_eq!(config.network_map.len(), 0);
401    }
402
403    #[test]
404    fn test_new_with_valid_inheritance() {
405        let networks = vec![
406            create_evm_network_wrapped("parent"),
407            create_evm_network_wrapped_with_parent("child", "parent"),
408        ];
409        let config = NetworksFileConfig::new(networks);
410
411        assert!(config.is_ok());
412        let config = config.unwrap();
413        assert_eq!(config.networks.len(), 2);
414    }
415
416    #[test]
417    fn test_new_with_invalid_inheritance_reference() {
418        let networks = vec![create_evm_network_wrapped_with_parent(
419            "child",
420            "non-existent",
421        )];
422        let result = NetworksFileConfig::new(networks);
423
424        assert!(result.is_err());
425        assert!(matches!(
426            result.unwrap_err(),
427            ConfigFileError::InvalidReference(_)
428        ));
429    }
430
431    #[test]
432    fn test_new_with_self_inheritance() {
433        let networks = vec![create_evm_network_wrapped_with_parent(
434            "self-ref", "self-ref",
435        )];
436        let result = NetworksFileConfig::new(networks);
437
438        assert!(result.is_err());
439        assert!(matches!(
440            result.unwrap_err(),
441            ConfigFileError::InvalidReference(_)
442        ));
443    }
444
445    #[test]
446    fn test_new_with_circular_inheritance() {
447        let networks = vec![
448            create_evm_network_wrapped_with_parent("a", "b"),
449            create_evm_network_wrapped_with_parent("b", "c"),
450            create_evm_network_wrapped_with_parent("c", "a"),
451        ];
452        let result = NetworksFileConfig::new(networks);
453
454        assert!(result.is_err());
455        assert!(matches!(
456            result.unwrap_err(),
457            ConfigFileError::CircularInheritance(_)
458        ));
459    }
460
461    #[test]
462    fn test_new_with_incompatible_inheritance_types() {
463        let networks = vec![
464            create_evm_network_wrapped("evm-parent"),
465            create_solana_network_wrapped_with_parent("solana-child", "evm-parent"),
466        ];
467        let result = NetworksFileConfig::new(networks);
468
469        assert!(result.is_err());
470        let error = result.unwrap_err();
471        assert!(matches!(error, ConfigFileError::InvalidReference(_)));
472    }
473
474    #[test]
475    fn test_new_with_deep_inheritance_chain() {
476        let networks = vec![
477            create_evm_network_wrapped("root"),
478            create_evm_network_wrapped_with_parent("level1", "root"),
479            create_evm_network_wrapped_with_parent("level2", "level1"),
480            create_evm_network_wrapped_with_parent("level3", "level2"),
481        ];
482        let config = NetworksFileConfig::new(networks);
483
484        assert!(config.is_ok());
485        let config = config.unwrap();
486        assert_eq!(config.networks.len(), 4);
487    }
488
489    #[test]
490    fn test_get_network_existing() {
491        let networks = vec![
492            create_evm_network_wrapped("test-evm"),
493            create_solana_network_wrapped("test-solana"),
494        ];
495        let config = NetworksFileConfig::new(networks).unwrap();
496
497        let network = config.get_network(ConfigFileNetworkType::Evm, "test-evm");
498        assert!(network.is_some());
499        assert_eq!(network.unwrap().network_name(), "test-evm");
500
501        let network = config.get_network(ConfigFileNetworkType::Solana, "test-solana");
502        assert!(network.is_some());
503        assert_eq!(network.unwrap().network_name(), "test-solana");
504    }
505
506    #[test]
507    fn test_get_network_non_existent() {
508        let networks = vec![create_evm_network_wrapped("test-evm")];
509        let config = NetworksFileConfig::new(networks).unwrap();
510
511        let network = config.get_network(ConfigFileNetworkType::Evm, "non-existent");
512        assert!(network.is_none());
513    }
514
515    #[test]
516    fn test_get_network_empty_config() {
517        let config = NetworksFileConfig::new(vec![]).unwrap();
518
519        let network = config.get_network(ConfigFileNetworkType::Evm, "any-name");
520        assert!(network.is_none());
521    }
522
523    #[test]
524    fn test_get_network_case_sensitive() {
525        let networks = vec![create_evm_network_wrapped("Test-Network")];
526        let config = NetworksFileConfig::new(networks).unwrap();
527
528        assert!(config
529            .get_network(ConfigFileNetworkType::Evm, "Test-Network")
530            .is_some());
531        assert!(config
532            .get_network(ConfigFileNetworkType::Evm, "test-network")
533            .is_none());
534        assert!(config
535            .get_network(ConfigFileNetworkType::Evm, "TEST-NETWORK")
536            .is_none());
537    }
538
539    #[test]
540    fn test_validate_success() {
541        let networks = vec![
542            create_evm_network_wrapped("evm-1"),
543            create_solana_network_wrapped("solana-1"),
544        ];
545        let config = NetworksFileConfig::new(networks).unwrap();
546
547        let result = config.validate();
548        assert!(result.is_ok());
549    }
550
551    #[test]
552    fn test_validate_with_invalid_network() {
553        let networks = vec![
554            create_evm_network_wrapped("valid"),
555            create_invalid_evm_network_wrapped("invalid"),
556        ];
557        let config = NetworksFileConfig::new(networks).unwrap();
558
559        let result = config.validate();
560        assert!(result.is_err());
561        assert!(matches!(
562            result.unwrap_err(),
563            ConfigFileError::MissingField(_)
564        ));
565    }
566
567    #[test]
568    fn test_validate_empty_config() {
569        let config = NetworksFileConfig::new(vec![]).unwrap();
570
571        let result = config.validate();
572        assert!(result.is_ok()); // Empty config is valid for validation
573    }
574
575    #[test]
576    fn test_flatten_without_inheritance() {
577        let networks = vec![
578            create_evm_network_wrapped("evm-1"),
579            create_solana_network_wrapped("solana-1"),
580        ];
581        let config = NetworksFileConfig::new(networks).unwrap();
582
583        let flattened = config.flatten();
584        assert!(flattened.is_ok());
585        let flattened = flattened.unwrap();
586        assert_eq!(flattened.networks.len(), 2);
587    }
588
589    #[test]
590    fn test_flatten_with_simple_inheritance() {
591        let networks = vec![
592            create_evm_network_wrapped("parent"),
593            create_evm_network_wrapped_with_parent("child", "parent"),
594        ];
595        let config = NetworksFileConfig::new(networks).unwrap();
596
597        let flattened = config.flatten();
598        assert!(flattened.is_ok());
599        let flattened = flattened.unwrap();
600        assert_eq!(flattened.networks.len(), 2);
601
602        // Child should still exist with inheritance information preserved
603        let child = flattened.get_network(ConfigFileNetworkType::Evm, "child");
604        assert!(child.is_some());
605        // The from field is preserved to show inheritance source, but inheritance is resolved
606        assert_eq!(child.unwrap().inherits_from(), Some("parent"));
607    }
608
609    #[test]
610    fn test_flatten_with_multi_level_inheritance() {
611        let networks = vec![
612            create_evm_network_wrapped("root"),
613            create_evm_network_wrapped_with_parent("middle", "root"),
614            create_evm_network_wrapped_with_parent("leaf", "middle"),
615        ];
616        let config = NetworksFileConfig::new(networks).unwrap();
617
618        let flattened = config.flatten();
619        assert!(flattened.is_ok());
620        let flattened = flattened.unwrap();
621        assert_eq!(flattened.networks.len(), 3);
622    }
623
624    #[test]
625    fn test_flatten_with_validation_failure() {
626        let networks = vec![
627            create_evm_network_wrapped("valid"),
628            create_invalid_evm_network_wrapped("invalid"),
629        ];
630        let config = NetworksFileConfig::new(networks).unwrap();
631
632        let result = config.flatten();
633        assert!(result.is_err());
634        assert!(matches!(
635            result.unwrap_err(),
636            ConfigFileError::MissingField(_)
637        ));
638    }
639
640    #[test]
641    fn test_flatten_with_mixed_network_types() {
642        let networks = vec![
643            create_evm_network_wrapped("evm-parent"),
644            create_evm_network_wrapped_with_parent("evm-child", "evm-parent"),
645            create_solana_network_wrapped("solana-parent"),
646            create_solana_network_wrapped_with_parent("solana-child", "solana-parent"),
647            create_stellar_network_wrapped("stellar-standalone"),
648        ];
649        let config = NetworksFileConfig::new(networks).unwrap();
650
651        let flattened = config.flatten();
652        assert!(flattened.is_ok());
653        let flattened = flattened.unwrap();
654        assert_eq!(flattened.networks.len(), 5);
655    }
656
657    #[test]
658    fn test_iter() {
659        let networks = vec![
660            create_evm_network_wrapped("evm-1"),
661            create_solana_network_wrapped("solana-1"),
662        ];
663        let config = NetworksFileConfig::new(networks).unwrap();
664
665        let collected: Vec<_> = config.iter().collect();
666        assert_eq!(collected.len(), 2);
667        assert_eq!(collected[0].network_name(), "evm-1");
668        assert_eq!(collected[1].network_name(), "solana-1");
669    }
670
671    #[test]
672    fn test_len() {
673        let config = NetworksFileConfig::new(vec![]).unwrap();
674        assert_eq!(config.len(), 0);
675
676        let networks = vec![create_evm_network_wrapped("test")];
677        let config = NetworksFileConfig::new(networks).unwrap();
678        assert_eq!(config.len(), 1);
679
680        let networks = vec![
681            create_evm_network_wrapped("test1"),
682            create_solana_network_wrapped("test2"),
683            create_stellar_network_wrapped("test3"),
684        ];
685        let config = NetworksFileConfig::new(networks).unwrap();
686        assert_eq!(config.len(), 3);
687    }
688
689    #[test]
690    fn test_is_empty() {
691        let config = NetworksFileConfig::new(vec![]).unwrap();
692        assert!(config.is_empty());
693
694        let networks = vec![create_evm_network_wrapped("test")];
695        let config = NetworksFileConfig::new(networks).unwrap();
696        assert!(!config.is_empty());
697    }
698
699    #[test]
700    fn test_networks_by_type() {
701        let networks = vec![
702            create_evm_network_wrapped("evm-1"),
703            create_evm_network_wrapped("evm-2"),
704            create_solana_network_wrapped("solana-1"),
705            create_stellar_network_wrapped("stellar-1"),
706        ];
707        let config = NetworksFileConfig::new(networks).unwrap();
708
709        let evm_networks: Vec<_> = config
710            .networks_by_type(ConfigFileNetworkType::Evm)
711            .collect();
712        assert_eq!(evm_networks.len(), 2);
713
714        let solana_networks: Vec<_> = config
715            .networks_by_type(ConfigFileNetworkType::Solana)
716            .collect();
717        assert_eq!(solana_networks.len(), 1);
718
719        let stellar_networks: Vec<_> = config
720            .networks_by_type(ConfigFileNetworkType::Stellar)
721            .collect();
722        assert_eq!(stellar_networks.len(), 1);
723    }
724
725    #[test]
726    fn test_networks_by_type_empty_result() {
727        let networks = vec![create_evm_network_wrapped("evm-only")];
728        let config = NetworksFileConfig::new(networks).unwrap();
729
730        let solana_networks: Vec<_> = config
731            .networks_by_type(ConfigFileNetworkType::Solana)
732            .collect();
733        assert_eq!(solana_networks.len(), 0);
734    }
735
736    #[test]
737    fn test_network_names() {
738        let networks = vec![
739            create_evm_network_wrapped("alpha"),
740            create_solana_network_wrapped("beta"),
741            create_stellar_network_wrapped("gamma"),
742        ];
743        let config = NetworksFileConfig::new(networks).unwrap();
744
745        let names: Vec<_> = config.network_names().collect();
746        assert_eq!(names.len(), 3);
747        assert!(names.contains(&"alpha"));
748        assert!(names.contains(&"beta"));
749        assert!(names.contains(&"gamma"));
750    }
751
752    #[test]
753    fn test_network_names_empty() {
754        let config = NetworksFileConfig::new(vec![]).unwrap();
755
756        let names: Vec<_> = config.network_names().collect();
757        assert_eq!(names.len(), 0);
758    }
759
760    // Tests for Default implementation
761    #[test]
762    fn test_default() {
763        let config = NetworksFileConfig::default();
764
765        assert_eq!(config.networks.len(), 0);
766        assert_eq!(config.network_map.len(), 0);
767        assert!(config.is_empty());
768    }
769
770    #[test]
771    fn test_deserialize_from_array() {
772        let json = r#"[
773            {
774                "type": "evm",
775                "network": "test-evm",
776                "chain_id": 31337,
777                "required_confirmations": 1,
778                "symbol": "ETH",
779                "rpc_urls": ["https://rpc.test.example.com"]
780            }
781        ]"#;
782
783        let result: Result<NetworksFileConfig, _> = serde_json::from_str(json);
784        assert!(result.is_ok());
785        let config = result.unwrap();
786        assert_eq!(config.len(), 1);
787        assert!(config
788            .get_network(ConfigFileNetworkType::Evm, "test-evm")
789            .is_some());
790    }
791
792    #[test]
793    fn test_deserialize_empty_array_returns_error() {
794        let json = r#"[]"#;
795        let result: Result<NetworksFileConfig, _> = serde_json::from_str(json);
796
797        assert!(result.is_err());
798        let error_message = result.unwrap_err().to_string();
799        assert!(error_message.contains("NetworksFileConfig cannot be empty"));
800    }
801
802    #[test]
803    fn test_deserialize_from_directory() {
804        let dir = tempdir().expect("Failed to create temp dir");
805        let network_dir_path = dir.path();
806
807        // Create test network files
808        let evm_file = network_dir_path.join("evm.json");
809        let mut file = File::create(&evm_file).expect("Failed to create EVM file");
810        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");
811
812        let solana_file = network_dir_path.join("solana.json");
813        let mut file = File::create(&solana_file).expect("Failed to create Solana file");
814        writeln!(file, r#"{{"networks": [{{"type": "solana", "network": "test-solana-from-file", "rpc_urls": ["https://rpc.solana.example.com"]}}]}}"#).expect("Failed to write Solana file");
815
816        let json = format!(r#""{}""#, network_dir_path.to_str().unwrap());
817
818        let result: Result<NetworksFileConfig, _> = serde_json::from_str(&json);
819        assert!(result.is_ok());
820        let config = result.unwrap();
821        assert_eq!(config.len(), 2);
822        assert!(config
823            .get_network(ConfigFileNetworkType::Evm, "test-evm-from-file")
824            .is_some());
825        assert!(config
826            .get_network(ConfigFileNetworkType::Solana, "test-solana-from-file")
827            .is_some());
828    }
829
830    #[test]
831    fn test_deserialize_invalid_directory() {
832        let json = r#""/non/existent/directory""#;
833
834        let result: Result<NetworksFileConfig, _> = serde_json::from_str(json);
835        assert!(result.is_err());
836    }
837
838    #[test]
839    fn test_deserialize_with_inheritance_resolution() {
840        let json = r#"[
841            {
842                "type": "evm",
843                "network": "parent",
844                "chain_id": 31337,
845                "required_confirmations": 1,
846                "symbol": "ETH",
847                "rpc_urls": ["https://rpc.parent.example.com"]
848            },
849            {
850                "type": "evm",
851                "network": "child",
852                "from": "parent",
853                "chain_id": 31338,
854                "required_confirmations": 1,
855                "symbol": "ETH"
856            }
857        ]"#;
858
859        let result: Result<NetworksFileConfig, _> = serde_json::from_str(json);
860        assert!(result.is_ok());
861        let config = result.unwrap();
862        assert_eq!(config.len(), 2);
863
864        // After deserialization, inheritance should be resolved but from field preserved
865        let child = config
866            .get_network(ConfigFileNetworkType::Evm, "child")
867            .unwrap();
868        assert_eq!(child.inherits_from(), Some("parent")); // From field preserved
869
870        // Verify that child has inherited properties from parent
871        if let NetworkFileConfig::Evm(child_evm) = child {
872            assert!(child_evm.common.rpc_urls.is_some()); // Should have inherited RPC URLs
873            assert_eq!(child_evm.chain_id, Some(31338)); // Should have overridden chain_id
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}