ferron/config/
processing.rs

1use std::{
2  collections::{HashMap, HashSet},
3  error::Error,
4};
5
6use ferron_common::modules::ModuleLoader;
7
8use super::{ServerConfiguration, ServerConfigurationFilters};
9
10/// Merges configurations with same filters
11/// Combines server configurations with identical filters by merging their entries.
12///
13/// This function takes a vector of server configurations and combines those that have matching
14/// filter criteria (hostname, IP, port, location prefix, and error handler status).
15/// For configurations with identical filters, their entries are merged.
16pub fn merge_duplicates(mut server_configurations: Vec<ServerConfiguration>) -> Vec<ServerConfiguration> {
17  // The resulting list of unique configurations after merging
18  let mut server_configurations_without_duplicates = Vec::new();
19
20  // Process each configuration one by one
21  while !server_configurations.is_empty() {
22    // Take the first configuration from the list
23    let mut server_configuration = server_configurations.remove(0);
24    let mut server_configurations_index = 0;
25
26    // Compare this configuration with all remaining ones
27    while server_configurations_index < server_configurations.len() {
28      // Get the current configuration to compare with
29      let server_configuration_source = &server_configurations[server_configurations_index];
30
31      // Check if all filter criteria match exactly between the two configurations
32      if server_configuration_source.filters.is_host == server_configuration.filters.is_host
33        && server_configuration_source.filters.hostname == server_configuration.filters.hostname
34        && server_configuration_source.filters.ip == server_configuration.filters.ip
35        && server_configuration_source.filters.port == server_configuration.filters.port
36        && server_configuration_source.filters.condition == server_configuration.filters.condition
37        && server_configuration_source.filters.error_handler_status == server_configuration.filters.error_handler_status
38      {
39        // Clone the entries from the matching configuration
40        let mut cloned_hashmap = server_configuration_source.entries.clone();
41        let moved_hashmap_iterator = server_configuration.entries.into_iter();
42
43        // Merge entries from both configurations
44        for (property_name, mut property) in moved_hashmap_iterator {
45          match cloned_hashmap.get_mut(&property_name) {
46            Some(obtained_property) => {
47              // If property exists in both configurations, combine their values
48              obtained_property.inner.append(&mut property.inner);
49            }
50            None => {
51              // If property only exists in current configuration, add it
52              cloned_hashmap.insert(property_name, property);
53            }
54          }
55        }
56
57        // Update entries with merged result
58        server_configuration.entries = cloned_hashmap;
59
60        // Remove the processed configuration from the list
61        server_configurations.remove(server_configurations_index);
62      } else {
63        // Move to next configuration if no match
64        server_configurations_index += 1;
65      }
66    }
67
68    // Add the processed configuration (with any merged entries) to the result list
69    server_configurations_without_duplicates.push(server_configuration);
70  }
71
72  // Return the deduplicated configurations
73  server_configurations_without_duplicates
74}
75
76/// Removes empty Ferron configurations and add an empty global configuration, if not present
77/// Ensures there is a global configuration in the server configurations.
78///
79/// This function filters out empty configurations, checks if a global configuration exists,
80/// and adds one if it doesn't.
81pub fn remove_and_add_global_configuration(
82  server_configurations: Vec<ServerConfiguration>,
83) -> Vec<ServerConfiguration> {
84  // The resulting list of server configurations
85  let mut new_server_configurations = Vec::new();
86  // Flag to track if a global non-host configuration exists
87  let mut has_global_non_host = false;
88
89  // Process each server configuration
90  for server_configuration in server_configurations {
91    // Only keep non-empty configurations
92    if !server_configuration.entries.is_empty() {
93      // Check if this is a global non-host configuration
94      if server_configuration.filters.is_global_non_host() {
95        has_global_non_host = true;
96      }
97      // Add the configuration to the result list
98      new_server_configurations.push(server_configuration);
99    }
100  }
101
102  // If no global non-host configuration exists, add a default one at the beginning
103  if !has_global_non_host {
104    new_server_configurations.insert(
105      0,
106      ServerConfiguration {
107        entries: HashMap::new(),
108        filters: ServerConfigurationFilters {
109          is_host: false,
110          hostname: None,
111          ip: None,
112          port: None,
113          condition: None,
114          error_handler_status: None,
115        },
116        modules: vec![],
117      },
118    );
119  }
120
121  // Return the processed configurations
122  new_server_configurations
123}
124
125/// Pre-merges Ferron configurations
126/// Merges server configurations based on a hierarchical inheritance model.
127///
128/// This function implements a layered configuration system where more specific configurations
129/// inherit and override properties from less specific ones. It handles matching logic based
130/// on specificity of filters (error handlers, location prefixes, hostnames, IPs, ports).
131pub fn premerge_configuration(mut server_configurations: Vec<ServerConfiguration>) -> Vec<ServerConfiguration> {
132  // Sort server configurations vector, based on the ascending specifity, to simplify the merging algorithm
133  server_configurations.sort_by(|a, b| a.filters.cmp(&b.filters));
134  let mut new_server_configurations = Vec::new();
135
136  // Process configurations from most specific to least specific
137  while let Some(mut server_configuration) = server_configurations.pop() {
138    // Track which configurations should be merged into the current one
139    let mut layers_indexes = Vec::new();
140    // Check each remaining configuration in reverse order (from most to least specific)
141    for sc2_index in (0..server_configurations.len()).rev() {
142      // A bit complex matching logic to determine inheritance relationships...
143      // sc1 is the current configuration, sc2 is the potential parent configuration
144      let sc1 = &server_configuration.filters;
145      let sc2 = &server_configurations[sc2_index].filters;
146
147      // Determine if filter criteria match or if parent has wildcard (None) values
148      // A None in parent (sc2) means it matches any value in child (sc1)
149      let is_host_match = !sc2.is_host || sc1.is_host == sc2.is_host;
150      let ports_match = sc2.port.is_none() || sc1.port == sc2.port;
151      let ips_match = sc2.ip.is_none() || sc1.ip == sc2.ip;
152      let hostnames_match = sc2.hostname.is_none() || sc1.hostname == sc2.hostname;
153      let conditions_match = sc2.condition.is_none() || sc1.condition == sc2.condition;
154
155      // Case 1: Child has error handler but parent doesn't, and all other filters match
156      // This is for error handler inheritance
157      let case1 = sc1.error_handler_status.is_some()
158        && sc2.error_handler_status.is_none()
159        && conditions_match
160        && hostnames_match
161        && ips_match
162        && ports_match
163        && is_host_match;
164
165      // Case 2: Condition inheritance
166      // Child has condition but parent doesn't, and all other filters match
167      let case2 = sc1.error_handler_status.is_none()
168        && sc2.error_handler_status.is_none()
169        && sc1.condition.is_some()
170        && sc2.condition.is_none()
171        && hostnames_match
172        && ips_match
173        && ports_match
174        && is_host_match;
175
176      // Case 3: Hostname inheritance
177      // Child has hostname but parent doesn't, and all other filters match
178      let case3 = sc1.error_handler_status.is_none()
179        && sc2.error_handler_status.is_none()
180        && sc1.condition.is_none()
181        && sc2.condition.is_none()
182        && sc1.hostname.is_some()
183        && sc2.hostname.is_none()
184        && ips_match
185        && ports_match
186        && is_host_match;
187
188      // Case 4: IP address inheritance
189      // Child has IP but parent doesn't, and all other filters match
190      let case4 = sc1.error_handler_status.is_none()
191        && sc2.error_handler_status.is_none()
192        && sc1.condition.is_none()
193        && sc2.condition.is_none()
194        && sc1.hostname.is_none()
195        && sc2.hostname.is_none()
196        && sc1.ip.is_some()
197        && sc2.ip.is_none()
198        && ports_match
199        && is_host_match;
200
201      // Case 5: Port inheritance
202      // Child has port but parent doesn't, and all other filters are None
203      let case5 = sc1.error_handler_status.is_none()
204        && sc2.error_handler_status.is_none()
205        && sc1.condition.is_none()
206        && sc2.condition.is_none()
207        && sc1.hostname.is_none()
208        && sc2.hostname.is_none()
209        && sc1.ip.is_none()
210        && sc2.ip.is_none()
211        && sc1.port.is_some()
212        && sc2.port.is_none()
213        && is_host_match;
214
215      // Case 6: Host block flag inheritance
216      // Child has host block flag but parent doesn't, and all other filters are None
217      let case6 = sc1.error_handler_status.is_none()
218        && sc2.error_handler_status.is_none()
219        && sc1.condition.is_none()
220        && sc2.condition.is_none()
221        && sc1.hostname.is_none()
222        && sc2.hostname.is_none()
223        && sc1.ip.is_none()
224        && sc2.ip.is_none()
225        && sc1.port.is_none()
226        && sc2.port.is_none()
227        && sc1.is_host
228        && !sc2.is_host;
229
230      // If any inheritance case matches, this configuration should inherit from the parent
231      if case1 || case2 || case3 || case4 || case5 || case6 {
232        layers_indexes.push(sc2_index);
233      }
234    }
235
236    // Start with current configuration's entries
237    let mut configuration_entries = server_configuration.entries;
238
239    // Process all parent configurations that this one should inherit from
240    for layer_index in layers_indexes {
241      // Track which properties have been processed in this layer
242      let mut properties_in_layer = HashSet::new();
243      // Clone parent configuration's entries
244      let mut cloned_hashmap = server_configurations[layer_index].entries.clone();
245      // Iterate through child configuration's entries
246      let moved_hashmap_iterator = configuration_entries.into_iter();
247      // Merge child entries with parent entries
248      for (property_name, mut property) in moved_hashmap_iterator {
249        match cloned_hashmap.get_mut(&property_name) {
250          Some(obtained_property) => {
251            if properties_in_layer.contains(&property_name) {
252              // If property was already processed in this layer, append values
253              obtained_property.inner.append(&mut property.inner);
254            } else {
255              // If property appears for the first time, replace values
256              obtained_property.inner = property.inner;
257            }
258          }
259          None => {
260            // If property doesn't exist in parent, add it
261            cloned_hashmap.insert(property_name.clone(), property);
262          }
263        }
264        // Mark this property as processed in this layer
265        properties_in_layer.insert(property_name);
266      }
267      // Update entries with merged result
268      configuration_entries = cloned_hashmap;
269    }
270    // Assign the merged entries back to the configuration
271    server_configuration.entries = configuration_entries;
272
273    // Add the processed configuration to the result list
274    new_server_configurations.push(server_configuration);
275  }
276
277  // Reverse the result to restore original specificity order
278  new_server_configurations.reverse();
279  new_server_configurations
280}
281
282/// Loads Ferron modules into its configurations
283/// Loads and validates modules for each server configuration.
284///
285/// This function processes each server configuration, validates it against available modules,
286/// and loads modules that meet their requirements. It tracks unused properties and any errors
287/// that occur during module loading.
288pub fn load_modules(
289  server_configurations: Vec<ServerConfiguration>,
290  server_modules: &mut [Box<dyn ModuleLoader + Send + Sync>],
291  secondary_runtime: &tokio::runtime::Runtime,
292) -> (
293  Vec<ServerConfiguration>,
294  Option<Box<dyn Error + Send + Sync>>,
295  Vec<String>,
296) {
297  // The resulting list of server configurations with loaded modules
298  let mut new_server_configurations = Vec::new();
299  // The first error encountered during module loading (if any)
300  let mut first_server_module_error = None;
301  // Properties that weren't used by any module
302  let mut unused_properties = HashSet::new();
303
304  // Find the global configuration to pass to modules
305  let global_configuration = find_global_configuration(&server_configurations);
306
307  // Process each server configuration
308  for mut server_configuration in server_configurations {
309    // Track which properties are used by modules
310    let mut used_properties = HashSet::new();
311
312    // Process each available server module
313    for server_module in server_modules.iter_mut() {
314      // Get module requirements
315      let requirements = server_module.get_requirements();
316      // Check if this module's requirements are satisfied by this configuration
317      let mut requirements_met = true;
318      for requirement in requirements {
319        requirements_met = false;
320        // Check if the required property exists and has a non-null value
321        if server_configuration
322          .entries
323          .get(requirement)
324          .and_then(|e| e.get_value())
325          .is_some_and(|v| !v.is_null() && v.as_bool().unwrap_or(true))
326        {
327          requirements_met = true;
328          break;
329        }
330      }
331      // Validate the configuration against this module
332      match server_module.validate_configuration(&server_configuration, &mut used_properties) {
333        Ok(_) => (),
334        Err(error) => {
335          // Store the first error encountered
336          if first_server_module_error.is_none() {
337            first_server_module_error.replace(error);
338          }
339          // Skip remaining modules for this configuration if validation fails
340          break;
341        }
342      }
343      // Only load module if its requirements are met
344      if requirements_met {
345        // Load the module with current configuration and global configuration
346        match server_module.load_module(&server_configuration, global_configuration.as_ref(), secondary_runtime) {
347          Ok(loaded_module) => server_configuration.modules.push(loaded_module),
348          Err(error) => {
349            // Store the first error encountered
350            if first_server_module_error.is_none() {
351              first_server_module_error.replace(error);
352            }
353            // Skip remaining modules for this configuration if loading fails
354            break;
355          }
356        }
357      }
358    }
359
360    // Track unused properties (except for undocumented ones)
361    for property in server_configuration.entries.keys() {
362      if !property.starts_with("UNDOCUMENTED_") && !used_properties.contains(property) {
363        unused_properties.insert(property.to_string());
364      }
365    }
366
367    // Add the configuration with loaded modules to the result list
368    new_server_configurations.push(server_configuration);
369  }
370  // Return:
371  // 1. Server configurations with modules loaded
372  // 2. First error encountered (if any)
373  // 3. List of unused properties
374  (
375    new_server_configurations,
376    first_server_module_error,
377    unused_properties.into_iter().collect(),
378  )
379}
380
381/// Finds the global server configuration (host or non-host) from the given list of server configurations.
382fn find_global_configuration(server_configurations: &[ServerConfiguration]) -> Option<ServerConfiguration> {
383  // The server configurations are pre-merged, so we can simply return the found global configuration
384  let mut iterator = server_configurations.iter();
385  let first_found = iterator.find(|server_configuration| {
386    server_configuration.filters.is_global() || server_configuration.filters.is_global_non_host()
387  });
388  if let Some(first_found) = first_found {
389    if first_found.filters.is_global() {
390      return Some(first_found.clone());
391    }
392    for server_configuration in iterator {
393      if server_configuration.filters.is_global() {
394        return Some(server_configuration.clone());
395      } else if !server_configuration.filters.is_global_non_host() {
396        return Some(first_found.clone());
397      }
398    }
399  }
400  None
401}
402
403#[cfg(test)]
404mod tests {
405  use crate::config::*;
406
407  use super::*;
408  use std::collections::HashMap;
409  use std::net::{IpAddr, Ipv4Addr};
410
411  fn make_filters(
412    is_host: bool,
413    hostname: Option<&str>,
414    ip: Option<IpAddr>,
415    port: Option<u16>,
416    location_prefix: Option<&str>,
417    error_handler_status: Option<ErrorHandlerStatus>,
418  ) -> ServerConfigurationFilters {
419    ServerConfigurationFilters {
420      is_host,
421      hostname: hostname.map(String::from),
422      ip,
423      port,
424      condition: location_prefix.map(|prefix| Conditions {
425        location_prefix: prefix.to_string(),
426        conditionals: vec![],
427      }),
428      error_handler_status,
429    }
430  }
431
432  fn make_entry(values: Vec<ServerConfigurationValue>) -> ServerConfigurationEntries {
433    ServerConfigurationEntries {
434      inner: vec![ServerConfigurationEntry {
435        values,
436        props: HashMap::new(),
437      }],
438    }
439  }
440
441  fn make_entry_premerge(key: &str, value: ServerConfigurationValue) -> (String, ServerConfigurationEntries) {
442    let entry = ServerConfigurationEntry {
443      values: vec![value],
444      props: HashMap::new(),
445    };
446    (key.to_string(), ServerConfigurationEntries { inner: vec![entry] })
447  }
448
449  fn config_with_filters(
450    is_host: bool,
451    hostname: Option<&str>,
452    ip: Option<IpAddr>,
453    port: Option<u16>,
454    location_prefix: Option<&str>,
455    error_handler_status: Option<ErrorHandlerStatus>,
456    entries: Vec<(String, ServerConfigurationEntries)>,
457  ) -> ServerConfiguration {
458    ServerConfiguration {
459      filters: ServerConfigurationFilters {
460        is_host,
461        hostname: hostname.map(|s| s.to_string()),
462        ip,
463        port,
464        condition: location_prefix.map(|prefix| Conditions {
465          location_prefix: prefix.to_string(),
466          conditionals: vec![],
467        }),
468        error_handler_status,
469      },
470      entries: entries.into_iter().collect(),
471      modules: vec![],
472    }
473  }
474
475  #[test]
476  fn merges_identical_filters_and_combines_entries() {
477    let filters = make_filters(
478      true,
479      Some("example.com"),
480      Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
481      Some(8080),
482      Some("/api"),
483      Some(ErrorHandlerStatus::Status(404)),
484    );
485
486    let filters_2 = make_filters(
487      true,
488      Some("example.com"),
489      Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
490      Some(8080),
491      Some("/api"),
492      Some(ErrorHandlerStatus::Status(404)),
493    );
494
495    let mut config1_entries = HashMap::new();
496    config1_entries.insert(
497      "route".to_string(),
498      make_entry(vec![ServerConfigurationValue::String("v1".to_string())]),
499    );
500
501    let mut config2_entries = HashMap::new();
502    config2_entries.insert(
503      "route".to_string(),
504      make_entry(vec![ServerConfigurationValue::String("v2".to_string())]),
505    );
506
507    let config1 = ServerConfiguration {
508      filters: filters_2,
509      entries: config1_entries,
510      modules: vec![],
511    };
512
513    let config2 = ServerConfiguration {
514      filters,
515      entries: config2_entries,
516      modules: vec![],
517    };
518
519    let merged = merge_duplicates(vec![config1, config2]);
520    assert_eq!(merged.len(), 1);
521
522    let merged_entries = &merged[0].entries;
523    assert!(merged_entries.contains_key("route"));
524    let route_entry = merged_entries.get("route").unwrap();
525    let values: Vec<_> = route_entry.inner.iter().flat_map(|e| e.values.iter()).collect();
526    assert_eq!(values.len(), 2);
527    assert!(values.contains(&&ServerConfigurationValue::String("v1".into())));
528    assert!(values.contains(&&ServerConfigurationValue::String("v2".into())));
529  }
530
531  #[test]
532  fn does_not_merge_different_filters() {
533    let filters1 = make_filters(
534      true,
535      Some("example.com"),
536      Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
537      Some(8080),
538      Some("/api"),
539      None,
540    );
541
542    let filters2 = make_filters(
543      true,
544      Some("example.org"),
545      Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
546      Some(8080),
547      Some("/api"),
548      None,
549    );
550
551    let mut config1_entries = HashMap::new();
552    config1_entries.insert(
553      "route".to_string(),
554      make_entry(vec![ServerConfigurationValue::String("v1".to_string())]),
555    );
556
557    let mut config2_entries = HashMap::new();
558    config2_entries.insert(
559      "route".to_string(),
560      make_entry(vec![ServerConfigurationValue::String("v2".to_string())]),
561    );
562
563    let config1 = ServerConfiguration {
564      filters: filters1,
565      entries: config1_entries,
566      modules: vec![],
567    };
568
569    let config2 = ServerConfiguration {
570      filters: filters2,
571      entries: config2_entries,
572      modules: vec![],
573    };
574
575    let merged = merge_duplicates(vec![config1, config2]);
576    assert_eq!(merged.len(), 2);
577  }
578
579  #[test]
580  fn merges_entries_with_non_overlapping_keys() {
581    let filters = make_filters(
582      true,
583      Some("example.com"),
584      Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
585      Some(8080),
586      None,
587      None,
588    );
589
590    let filters_2 = make_filters(
591      true,
592      Some("example.com"),
593      Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
594      Some(8080),
595      None,
596      None,
597    );
598
599    let mut config1_entries = HashMap::new();
600    config1_entries.insert(
601      "route1".to_string(),
602      make_entry(vec![ServerConfigurationValue::String("r1".to_string())]),
603    );
604
605    let mut config2_entries = HashMap::new();
606    config2_entries.insert(
607      "route2".to_string(),
608      make_entry(vec![ServerConfigurationValue::String("r2".to_string())]),
609    );
610
611    let config1 = ServerConfiguration {
612      filters: filters_2,
613      entries: config1_entries,
614      modules: vec![],
615    };
616
617    let config2 = ServerConfiguration {
618      filters,
619      entries: config2_entries,
620      modules: vec![],
621    };
622
623    let merged = merge_duplicates(vec![config1, config2]);
624    assert_eq!(merged.len(), 1);
625
626    let merged_entries = &merged[0].entries;
627    assert_eq!(merged_entries.len(), 2);
628    assert!(merged_entries.contains_key("route1"));
629    assert!(merged_entries.contains_key("route2"));
630  }
631
632  #[test]
633  fn test_no_merge_returns_all() {
634    let config1 = config_with_filters(
635      true,
636      Some("example.com"),
637      Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
638      Some(80),
639      None,
640      None,
641      vec![make_entry_premerge(
642        "key1",
643        ServerConfigurationValue::String("val1".into()),
644      )],
645    );
646
647    let config2 = config_with_filters(
648      true,
649      Some("example.org"),
650      Some(IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1))),
651      Some(8080),
652      None,
653      None,
654      vec![make_entry_premerge(
655        "key2",
656        ServerConfigurationValue::String("val2".into()),
657      )],
658    );
659
660    let merged = premerge_configuration(vec![config1, config2]);
661
662    assert_eq!(merged.len(), 2);
663    assert!(merged.iter().any(|c| c.entries.contains_key("key1")));
664    assert!(merged.iter().any(|c| c.entries.contains_key("key2")));
665  }
666
667  #[test]
668  fn test_merge_case6_is_host() {
669    // Less specific config (no port)
670    let base = config_with_filters(
671      false,
672      None,
673      None,
674      None,
675      None,
676      None,
677      vec![make_entry_premerge(
678        "shared",
679        ServerConfigurationValue::String("base".into()),
680      )],
681    );
682
683    // More specific config (with port)
684    let specific = config_with_filters(
685      true,
686      None,
687      None,
688      None,
689      None,
690      None,
691      vec![make_entry_premerge(
692        "shared",
693        ServerConfigurationValue::String("specific".into()),
694      )],
695    );
696
697    let merged = premerge_configuration(vec![base, specific]);
698    assert_eq!(merged.len(), 2);
699
700    let entries = &merged[1].entries["shared"].inner;
701    assert_eq!(entries.len(), 1);
702    assert_eq!(entries[0].values[0].as_str(), Some("specific"));
703  }
704
705  #[test]
706  fn test_merge_case5_port() {
707    // Less specific config (no port)
708    let base = config_with_filters(
709      true,
710      None,
711      None,
712      None,
713      None,
714      None,
715      vec![make_entry_premerge(
716        "shared",
717        ServerConfigurationValue::String("base".into()),
718      )],
719    );
720
721    // More specific config (with port)
722    let specific = config_with_filters(
723      true,
724      None,
725      None,
726      Some(80),
727      None,
728      None,
729      vec![make_entry_premerge(
730        "shared",
731        ServerConfigurationValue::String("specific".into()),
732      )],
733    );
734
735    let merged = premerge_configuration(vec![base, specific]);
736    assert_eq!(merged.len(), 2);
737
738    let entries = &merged[1].entries["shared"].inner;
739    assert_eq!(entries.len(), 1);
740    assert_eq!(entries[0].values[0].as_str(), Some("specific"));
741  }
742
743  #[test]
744  fn test_merge_case1_error_handler() {
745    let base = config_with_filters(
746      true,
747      Some("host"),
748      Some(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4))),
749      Some(3000),
750      Some("/api"),
751      None,
752      vec![make_entry_premerge(
753        "eh",
754        ServerConfigurationValue::String("base".into()),
755      )],
756    );
757
758    let specific = config_with_filters(
759      true,
760      Some("host"),
761      Some(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4))),
762      Some(3000),
763      Some("/api"),
764      Some(ErrorHandlerStatus::Any),
765      vec![make_entry_premerge(
766        "eh",
767        ServerConfigurationValue::String("specific".into()),
768      )],
769    );
770
771    let merged = premerge_configuration(vec![base, specific]);
772    assert_eq!(merged.len(), 2);
773
774    let entries = &merged[1].entries["eh"].inner;
775    assert_eq!(entries.len(), 1);
776    assert_eq!(entries[0].values[0].as_str(), Some("specific"));
777  }
778
779  #[test]
780  fn test_merge_preserves_specificity_order() {
781    let configs = vec![
782      config_with_filters(
783        true,
784        None,
785        None,
786        None,
787        None,
788        None,
789        vec![make_entry_premerge("a", ServerConfigurationValue::String("v1".into()))],
790      ),
791      config_with_filters(
792        true,
793        None,
794        None,
795        Some(80),
796        None,
797        None,
798        vec![make_entry_premerge("a", ServerConfigurationValue::String("v2".into()))],
799      ),
800      config_with_filters(
801        true,
802        Some("host"),
803        None,
804        Some(80),
805        None,
806        None,
807        vec![make_entry_premerge("a", ServerConfigurationValue::String("v3".into()))],
808      ),
809    ];
810
811    let merged = premerge_configuration(configs);
812    assert_eq!(merged.len(), 3);
813
814    let entries = &merged[2].entries["a"].inner;
815    assert_eq!(entries.len(), 1);
816    assert_eq!(entries[0].values[0].as_str(), Some("v3"));
817  }
818}