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_with_inheritance_resolution() {
833        let json = r#"[
834            {
835                "type": "evm",
836                "network": "parent",
837                "chain_id": 31337,
838                "required_confirmations": 1,
839                "symbol": "ETH",
840                "rpc_urls": ["https://rpc.parent.example.com"]
841            },
842            {
843                "type": "evm",
844                "network": "child",
845                "from": "parent",
846                "chain_id": 31338,
847                "required_confirmations": 1,
848                "symbol": "ETH"
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        // After deserialization, inheritance should be resolved but from field preserved
858        let child = config
859            .get_network(ConfigFileNetworkType::Evm, "child")
860            .unwrap();
861        assert_eq!(child.inherits_from(), Some("parent")); // From field preserved
862
863        // Verify that child has inherited properties from parent
864        if let NetworkFileConfig::Evm(child_evm) = child {
865            assert!(child_evm.common.rpc_urls.is_some()); // Should have inherited RPC URLs
866            assert_eq!(child_evm.chain_id, Some(31338)); // Should have overridden chain_id
867        }
868    }
869
870    #[test]
871    fn test_deserialize_with_invalid_inheritance() {
872        let json = r#"[
873            {
874                "type": "evm",
875                "network": "child",
876                "from": "non-existent-parent",
877                "chain_id": 31337,
878                "required_confirmations": 1,
879                "symbol": "ETH",
880                "rpc_urls": ["https://rpc.test.example.com"]
881            }
882        ]"#;
883
884        let result: Result<NetworksFileConfig, _> = serde_json::from_str(json);
885        assert!(result.is_err());
886    }
887
888    // Edge cases and stress tests
889    #[test]
890    fn test_large_number_of_networks() {
891        let mut networks = Vec::new();
892        for i in 0..100 {
893            networks.push(create_evm_network_wrapped(&format!("network-{}", i)));
894        }
895
896        let config = NetworksFileConfig::new(networks);
897        assert!(config.is_ok());
898        let config = config.unwrap();
899        assert_eq!(config.len(), 100);
900
901        // Test that all networks are accessible
902        for i in 0..100 {
903            assert!(config
904                .get_network(ConfigFileNetworkType::Evm, &format!("network-{}", i))
905                .is_some());
906        }
907    }
908
909    #[test]
910    fn test_unicode_network_names() {
911        let networks = vec![
912            create_evm_network_wrapped("测试网络"),
913            create_solana_network_wrapped("тестовая-сеть"),
914            create_stellar_network_wrapped("réseau-test"),
915        ];
916
917        let config = NetworksFileConfig::new(networks);
918        assert!(config.is_ok());
919        let config = config.unwrap();
920        assert_eq!(config.len(), 3);
921        assert!(config
922            .get_network(ConfigFileNetworkType::Evm, "测试网络")
923            .is_some());
924        assert!(config
925            .get_network(ConfigFileNetworkType::Solana, "тестовая-сеть")
926            .is_some());
927        assert!(config
928            .get_network(ConfigFileNetworkType::Stellar, "réseau-test")
929            .is_some());
930    }
931
932    #[test]
933    fn test_special_characters_in_network_names() {
934        let networks = vec![
935            create_evm_network_wrapped("test-network_123"),
936            create_solana_network_wrapped("test.network.with.dots"),
937            create_stellar_network_wrapped("test@network#with$symbols"),
938        ];
939
940        let config = NetworksFileConfig::new(networks);
941        assert!(config.is_ok());
942        let config = config.unwrap();
943        assert_eq!(config.len(), 3);
944    }
945
946    #[test]
947    fn test_very_long_network_names() {
948        let long_name = "a".repeat(1000);
949        let networks = vec![create_evm_network_wrapped(&long_name)];
950
951        let config = NetworksFileConfig::new(networks);
952        assert!(config.is_ok());
953        let config = config.unwrap();
954        assert!(config
955            .get_network(ConfigFileNetworkType::Evm, &long_name)
956            .is_some());
957    }
958
959    #[test]
960    fn test_complex_inheritance_scenario() {
961        let networks = vec![
962            // Root networks
963            create_evm_network_wrapped("evm-root"),
964            create_solana_network_wrapped("solana-root"),
965            // First level children
966            create_evm_network_wrapped_with_parent("evm-child1", "evm-root"),
967            create_evm_network_wrapped_with_parent("evm-child2", "evm-root"),
968            create_solana_network_wrapped_with_parent("solana-child1", "solana-root"),
969            // Second level children
970            create_evm_network_wrapped_with_parent("evm-grandchild", "evm-child1"),
971        ];
972
973        let config = NetworksFileConfig::new(networks);
974        assert!(config.is_ok());
975        let config = config.unwrap();
976        assert_eq!(config.len(), 6);
977
978        let flattened = config.flatten();
979        assert!(flattened.is_ok());
980        let flattened = flattened.unwrap();
981        assert_eq!(flattened.len(), 6);
982    }
983
984    #[test]
985    fn test_new_with_duplicate_network_names_across_types() {
986        // Should allow same name across different network types
987        let networks = vec![
988            create_evm_network_wrapped("mainnet"),
989            create_solana_network_wrapped("mainnet"),
990            create_stellar_network_wrapped("mainnet"),
991        ];
992        let result = NetworksFileConfig::new(networks);
993
994        assert!(result.is_ok());
995        let config = result.unwrap();
996        assert_eq!(config.networks.len(), 3);
997        assert_eq!(config.network_map.len(), 3);
998
999        // Verify we can retrieve each network by type and name
1000        assert!(config
1001            .get_network(ConfigFileNetworkType::Evm, "mainnet")
1002            .is_some());
1003        assert!(config
1004            .get_network(ConfigFileNetworkType::Solana, "mainnet")
1005            .is_some());
1006        assert!(config
1007            .get_network(ConfigFileNetworkType::Stellar, "mainnet")
1008            .is_some());
1009    }
1010
1011    #[test]
1012    fn test_new_with_duplicate_network_names_within_same_type() {
1013        let networks = vec![
1014            create_evm_network_wrapped("duplicate-evm"),
1015            create_evm_network_wrapped("duplicate-evm"),
1016        ];
1017        let result = NetworksFileConfig::new(networks);
1018
1019        assert!(result.is_err());
1020        assert!(matches!(
1021            result.unwrap_err(),
1022            ConfigFileError::DuplicateId(_)
1023        ));
1024    }
1025
1026    #[test]
1027    fn test_get_with_empty_config() {
1028        let config = NetworksFileConfig::new(vec![]).unwrap();
1029
1030        let network_0 = config.get(0);
1031        assert!(network_0.is_none());
1032    }
1033
1034    #[test]
1035    fn test_get_and_first_equivalence() {
1036        let networks = vec![create_evm_network_wrapped("test-network")];
1037        let config = NetworksFileConfig::new(networks).unwrap();
1038
1039        // Both methods should return the same result
1040        let network_via_get = config.get(0);
1041        let network_via_first = config.first();
1042
1043        assert!(network_via_get.is_some());
1044        assert!(network_via_first.is_some());
1045        assert_eq!(
1046            network_via_get.unwrap().network_name(),
1047            network_via_first.unwrap().network_name()
1048        );
1049        assert_eq!(network_via_get.unwrap().network_name(), "test-network");
1050    }
1051
1052    #[test]
1053    #[allow(clippy::get_first)]
1054    fn test_different_access_methods() {
1055        let networks = vec![
1056            create_evm_network_wrapped("network-0"),
1057            create_solana_network_wrapped("network-1"),
1058        ];
1059        let config = NetworksFileConfig::new(networks).unwrap();
1060
1061        // Method 1: Using .get())
1062        let net_0_get = config.get(0);
1063        assert!(net_0_get.is_some());
1064        assert_eq!(net_0_get.unwrap().network_name(), "network-0");
1065
1066        // Method 2: Using .first()
1067        let net_0_first = config.first();
1068        assert!(net_0_first.is_some());
1069        assert_eq!(net_0_first.unwrap().network_name(), "network-0");
1070
1071        // Method 3: Using indexing [0] (Index trait)
1072        let net_0_index = &config[0];
1073        assert_eq!(net_0_index.network_name(), "network-0");
1074
1075        // Method 4: Using direct field access
1076        let net_0_direct = config.networks.get(0);
1077        assert!(net_0_direct.is_some());
1078        assert_eq!(net_0_direct.unwrap().network_name(), "network-0");
1079
1080        // All should reference the same network
1081        assert_eq!(
1082            net_0_get.unwrap().network_name(),
1083            net_0_first.unwrap().network_name()
1084        );
1085        assert_eq!(
1086            net_0_get.unwrap().network_name(),
1087            net_0_index.network_name()
1088        );
1089        assert_eq!(
1090            net_0_get.unwrap().network_name(),
1091            net_0_direct.unwrap().network_name()
1092        );
1093    }
1094
1095    #[test]
1096    fn test_first_with_non_empty_config() {
1097        let networks = vec![
1098            create_evm_network_wrapped("first-network"),
1099            create_solana_network_wrapped("second-network"),
1100        ];
1101        let config = NetworksFileConfig::new(networks).unwrap();
1102
1103        let first_network = config.first();
1104        assert!(first_network.is_some());
1105        assert_eq!(first_network.unwrap().network_name(), "first-network");
1106    }
1107
1108    #[test]
1109    fn test_first_with_empty_config() {
1110        let config = NetworksFileConfig::new(vec![]).unwrap();
1111
1112        let first_network = config.first();
1113        assert!(first_network.is_none());
1114    }
1115
1116    #[test]
1117    fn test_get_with_valid_index() {
1118        let networks = vec![
1119            create_evm_network_wrapped("network-0"),
1120            create_solana_network_wrapped("network-1"),
1121            create_stellar_network_wrapped("network-2"),
1122        ];
1123        let config = NetworksFileConfig::new(networks).unwrap();
1124
1125        let network_0 = config.get(0);
1126        assert!(network_0.is_some());
1127        assert_eq!(network_0.unwrap().network_name(), "network-0");
1128
1129        let network_1 = config.get(1);
1130        assert!(network_1.is_some());
1131        assert_eq!(network_1.unwrap().network_name(), "network-1");
1132
1133        let network_2 = config.get(2);
1134        assert!(network_2.is_some());
1135        assert_eq!(network_2.unwrap().network_name(), "network-2");
1136    }
1137
1138    #[test]
1139    fn test_get_with_invalid_index() {
1140        let networks = vec![create_evm_network_wrapped("only-network")];
1141        let config = NetworksFileConfig::new(networks).unwrap();
1142
1143        let network_out_of_bounds = config.get(1);
1144        assert!(network_out_of_bounds.is_none());
1145
1146        let network_large_index = config.get(100);
1147        assert!(network_large_index.is_none());
1148    }
1149
1150    #[test]
1151    fn test_networks_source_default() {
1152        let default_source = NetworksSource::default();
1153        match default_source {
1154            NetworksSource::Path(path) => {
1155                assert_eq!(path, "./config/networks");
1156            }
1157            _ => panic!("Default should be a Path variant"),
1158        }
1159    }
1160}