ferron/config/
processing.rs

1use std::{
2  collections::{BTreeMap, HashMap, HashSet, VecDeque},
3  error::Error,
4  net::IpAddr,
5};
6
7use ferron_common::{
8  config::{Conditional, ErrorHandlerStatus},
9  modules::ModuleLoader,
10  observability::{ObservabilityBackendChannels, ObservabilityBackendLoader},
11};
12
13use super::{ServerConfiguration, ServerConfigurationFilters};
14
15/// Merges configurations with same filters
16/// Combines server configurations with identical filters by merging their entries.
17///
18/// This function takes a vector of server configurations and combines those that have matching
19/// filter criteria (hostname, IP, port, location prefix, and error handler status).
20/// For configurations with identical filters, their entries are merged.
21pub fn merge_duplicates(mut server_configurations: Vec<ServerConfiguration>) -> Vec<ServerConfiguration> {
22  // Sort configurations by filter criteria
23  server_configurations.sort_by(|a, b| {
24    (
25      &a.filters.is_host,
26      &a.filters.port,
27      &a.filters.ip,
28      &a.filters.hostname,
29      &a.filters
30        .condition
31        .as_ref()
32        .map(|s| (&s.location_prefix, &s.conditionals)),
33      &a.filters.error_handler_status,
34    )
35      .cmp(&(
36        &b.filters.is_host,
37        &b.filters.port,
38        &b.filters.ip,
39        &b.filters.hostname,
40        &b.filters
41          .condition
42          .as_ref()
43          .map(|s| (&s.location_prefix, &s.conditionals)),
44        &b.filters.error_handler_status,
45      ))
46  });
47
48  // Convert server configurations to a double-ended queue
49  let mut server_configurations = VecDeque::from(server_configurations);
50
51  let mut result = Vec::new();
52  while !server_configurations.is_empty() {
53    if let Some(mut current) = server_configurations.pop_front() {
54      // Merge all adjacent configurations with matching filters
55      while !server_configurations.is_empty()
56        && server_configurations[0].filters.is_host == current.filters.is_host
57        && server_configurations[0].filters.hostname == current.filters.hostname
58        && server_configurations[0].filters.ip == current.filters.ip
59        && server_configurations[0].filters.port == current.filters.port
60        && server_configurations[0].filters.condition == current.filters.condition
61        && server_configurations[0].filters.error_handler_status == current.filters.error_handler_status
62      {
63        if let Some(server_configuration) = server_configurations.pop_front() {
64          // Merge entries
65          for (k, v) in server_configuration.entries {
66            current.entries.entry(k).or_default().inner.extend(v.inner);
67          }
68        }
69      }
70      result.push(current);
71    }
72  }
73
74  result
75}
76
77/// Removes empty Ferron configurations and add an empty global configuration, if not present
78/// Ensures there is a global configuration in the server configurations.
79///
80/// This function filters out empty configurations, checks if a global configuration exists,
81/// and adds one if it doesn't.
82pub fn remove_and_add_global_configuration(
83  server_configurations: Vec<ServerConfiguration>,
84) -> Vec<ServerConfiguration> {
85  // The resulting list of server configurations
86  let mut new_server_configurations = Vec::new();
87  // Flag to track if a global non-host configuration exists
88  let mut has_global_non_host = false;
89
90  // Process each server configuration
91  for server_configuration in server_configurations {
92    // Only keep non-empty configurations
93    if !server_configuration.entries.is_empty() {
94      // Check if this is a global non-host configuration
95      if server_configuration.filters.is_global_non_host() {
96        has_global_non_host = true;
97      }
98      // Add the configuration to the result list
99      new_server_configurations.push(server_configuration);
100    }
101  }
102
103  // If no global non-host configuration exists, add a default one at the beginning
104  if !has_global_non_host {
105    new_server_configurations.insert(
106      0,
107      ServerConfiguration {
108        entries: HashMap::new(),
109        filters: ServerConfigurationFilters {
110          is_host: false,
111          hostname: None,
112          ip: None,
113          port: None,
114          condition: None,
115          error_handler_status: None,
116        },
117        modules: vec![],
118        observability: ObservabilityBackendChannels::new(),
119      },
120    );
121  }
122
123  // Return the processed configurations
124  new_server_configurations
125}
126
127/// Configuration filter enum for a trie
128#[derive(Clone, PartialEq, PartialOrd, Eq, Ord)]
129enum ServerConfigurationFilter {
130  /// Whether the configuration represents a host block
131  IsHost(bool),
132
133  /// The port
134  Port(Option<u16>),
135
136  /// The IP address
137  Ip(Option<IpAddr>),
138
139  /// The hostname
140  Hostname(Option<String>),
141
142  /// The conditions
143  Condition(Option<(String, Vec<Conditional>)>),
144
145  /// The error handler status code
146  ErrorHandlerStatus(Option<ErrorHandlerStatus>),
147}
148
149/// Configuration filter trie
150struct ServerConfigurationFilterTrie {
151  children: BTreeMap<ServerConfigurationFilter, ServerConfigurationFilterTrie>,
152  index: Option<usize>,
153}
154
155impl ServerConfigurationFilterTrie {
156  /// Creates an empty ConfigurationFilterTrie.
157  pub fn new() -> Self {
158    Self {
159      children: BTreeMap::new(),
160      index: None,
161    }
162  }
163
164  /// Inserts new filters with index into the trie.
165  pub fn insert(&mut self, filters: ServerConfigurationFilters, filters_index: usize) {
166    let no_host = !filters.is_host;
167    let no_port = filters.port.is_none();
168    let no_ip = filters.ip.is_none();
169    let no_hostname = filters.hostname.is_none();
170    let no_condition = filters.condition.is_none();
171    let no_error_handler_status = filters.error_handler_status.is_none();
172
173    let filter_vec = vec![
174      ServerConfigurationFilter::IsHost(filters.is_host),
175      ServerConfigurationFilter::Port(filters.port),
176      ServerConfigurationFilter::Ip(filters.ip),
177      ServerConfigurationFilter::Hostname(filters.hostname),
178      ServerConfigurationFilter::Condition(filters.condition.map(|s| (s.location_prefix, s.conditionals))),
179      ServerConfigurationFilter::ErrorHandlerStatus(filters.error_handler_status),
180    ];
181
182    let mut current_node = self;
183    for filter in filter_vec {
184      if match &filter {
185        ServerConfigurationFilter::IsHost(_) => {
186          no_host && no_port && no_ip && no_hostname && no_condition && no_error_handler_status
187        }
188        ServerConfigurationFilter::Port(_) => {
189          no_port && no_ip && no_hostname && no_condition && no_error_handler_status
190        }
191        ServerConfigurationFilter::Ip(_) => no_ip && no_hostname && no_condition && no_error_handler_status,
192        ServerConfigurationFilter::Hostname(_) => no_hostname && no_condition && no_error_handler_status,
193        ServerConfigurationFilter::Condition(_) => no_condition && no_error_handler_status,
194        ServerConfigurationFilter::ErrorHandlerStatus(_) => no_error_handler_status,
195      } && current_node.index.is_none()
196      {
197        current_node.index = Some(filters_index);
198      }
199      if !current_node.children.contains_key(&filter) {
200        current_node.children.insert(filter.clone(), Self::new());
201      }
202      match current_node.children.get_mut(&filter) {
203        Some(node) => current_node = node,
204        None => unreachable!(),
205      }
206    }
207  }
208
209  /// Finds indices by the filters in the trie.
210  pub fn find_indices(&self, filters: ServerConfigurationFilters) -> Vec<usize> {
211    let filter_vec = vec![
212      ServerConfigurationFilter::IsHost(filters.is_host),
213      ServerConfigurationFilter::Port(filters.port),
214      ServerConfigurationFilter::Ip(filters.ip),
215      ServerConfigurationFilter::Hostname(filters.hostname),
216      ServerConfigurationFilter::Condition(filters.condition.map(|s| (s.location_prefix, s.conditionals))),
217      ServerConfigurationFilter::ErrorHandlerStatus(filters.error_handler_status),
218    ];
219
220    let mut current_node = self;
221    let mut indices = Vec::new();
222    for filter in filter_vec {
223      if indices.last() != current_node.index.as_ref() {
224        if let Some(index) = current_node.index {
225          indices.push(index);
226        }
227      }
228      let child = current_node.children.get(&filter);
229      match child {
230        Some(child) => {
231          current_node = child;
232        }
233        None => break,
234      }
235    }
236    indices.reverse();
237    indices
238  }
239}
240
241/// Pre-merges Ferron configurations
242/// Merges server configurations based on a hierarchical inheritance model.
243///
244/// This function implements a layered configuration system where more specific configurations
245/// inherit and override properties from less specific ones. It handles matching logic based
246/// on specificity of filters (error handlers, location prefixes, hostnames, IPs, ports).
247pub fn premerge_configuration(mut server_configurations: Vec<ServerConfiguration>) -> Vec<ServerConfiguration> {
248  // Sort server configurations vector, based on the ascending specifity, to make the algorithm easier to implement
249  server_configurations.sort_by(|a, b| a.filters.cmp(&b.filters));
250
251  // Initialize a trie to store server configurations based on their filters
252  let mut server_configuration_filter_trie = ServerConfigurationFilterTrie::new();
253  for (index, server_configuration) in server_configurations.iter().enumerate() {
254    server_configuration_filter_trie.insert(server_configuration.filters.clone(), index);
255  }
256
257  // Initialize a vector to store the new server configurations
258  let mut new_server_configurations = Vec::with_capacity(server_configurations.len());
259
260  // Pre-merge server configurations
261  while let Some(mut server_configuration) = server_configurations.pop() {
262    // Get the layers indexes
263    let layers_indexes = server_configuration_filter_trie.find_indices(server_configuration.filters.clone());
264
265    // Start with current configuration's entries
266    let mut configuration_entries = server_configuration.entries;
267
268    // Process all parent configurations that this one should inherit from
269    for layer_index in layers_indexes {
270      // If layer index is out of bounds, skip it
271      if layer_index >= server_configurations.len() {
272        continue;
273      }
274
275      // Track which properties have been processed in this layer
276      let mut properties_in_layer = HashSet::new();
277      // Clone parent configuration's entries
278      let mut cloned_hashmap = server_configurations[layer_index].entries.clone();
279      // Iterate through child configuration's entries
280      let moved_hashmap_iterator = configuration_entries.into_iter();
281      // Merge child entries with parent entries
282      for (property_name, mut property) in moved_hashmap_iterator {
283        match cloned_hashmap.get_mut(&property_name) {
284          Some(obtained_property) => {
285            if properties_in_layer.contains(&property_name) {
286              // If property was already processed in this layer, append values
287              obtained_property.inner.append(&mut property.inner);
288            } else {
289              // If property appears for the first time, replace values
290              obtained_property.inner = property.inner;
291            }
292          }
293          None => {
294            // If property doesn't exist in parent, add it
295            cloned_hashmap.insert(property_name.clone(), property);
296          }
297        }
298        // Mark this property as processed in this layer
299        properties_in_layer.insert(property_name);
300      }
301      // Update entries with merged result
302      configuration_entries = cloned_hashmap;
303    }
304    // Assign the merged entries back to the configuration
305    server_configuration.entries = configuration_entries;
306
307    // Add the processed configuration to the result list
308    new_server_configurations.push(server_configuration);
309  }
310
311  // Reverse the result to restore original specificity order
312  new_server_configurations.reverse();
313  new_server_configurations
314}
315
316/// Loads Ferron modules into its configurations
317/// Loads and validates modules for each server configuration.
318///
319/// This function processes each server configuration, validates it against available modules,
320/// and loads modules that meet their requirements. It tracks unused properties and any errors
321/// that occur during module loading.
322pub fn load_modules(
323  server_configurations: Vec<ServerConfiguration>,
324  server_modules: &mut [Box<dyn ModuleLoader + Send + Sync>],
325  server_observability_backends: &mut [Box<dyn ObservabilityBackendLoader + Send + Sync>],
326  secondary_runtime: &tokio::runtime::Runtime,
327) -> (
328  Vec<ServerConfiguration>,
329  Option<Box<dyn Error + Send + Sync>>,
330  Vec<String>,
331) {
332  // The resulting list of server configurations with loaded modules
333  let mut new_server_configurations = Vec::new();
334  // The first error encountered during module loading (if any)
335  let mut first_server_module_error = None;
336  // Properties that weren't used by any module
337  let mut unused_properties = HashSet::new();
338
339  // Find the global configuration to pass to modules
340  let global_configuration = find_global_configuration(&server_configurations);
341
342  // Process each server configuration
343  for mut server_configuration in server_configurations {
344    // Track which properties are used by modules
345    let mut used_properties = HashSet::new();
346
347    // Process each available observability backend
348    for server_observability_backend in server_observability_backends.iter_mut() {
349      // Get observability backend requirements
350      let requirements = server_observability_backend.get_requirements();
351      // Check if this observability backend's requirements are satisfied by this configuration
352      let mut requirements_met = true;
353      for requirement in requirements {
354        requirements_met = false;
355        // Check if the required property exists and has a non-null value
356        if server_configuration
357          .entries
358          .get(requirement)
359          .and_then(|e| e.get_value())
360          .is_some_and(|v| !v.is_null() && v.as_bool().unwrap_or(true))
361        {
362          requirements_met = true;
363          break;
364        }
365      }
366      // Validate the configuration against this observability backend
367      match server_observability_backend.validate_configuration(&server_configuration, &mut used_properties) {
368        Ok(_) => (),
369        Err(error) => {
370          // Store the first error encountered
371          if first_server_module_error.is_none() {
372            first_server_module_error
373              .replace(anyhow::anyhow!("{error} (at {})", server_configuration.filters).into_boxed_dyn_error());
374          }
375          // Skip remaining observability backends for this configuration if validation fails
376          break;
377        }
378      }
379      // Only load observability backend if its requirements are met
380      if requirements_met {
381        // Load the observability backend with current configuration and global configuration
382        match server_observability_backend.load_observability_backend(
383          &server_configuration,
384          global_configuration.as_ref(),
385          secondary_runtime,
386        ) {
387          Ok(loaded_observability_backend) => {
388            if let Some(channel) = loaded_observability_backend.get_log_channel() {
389              server_configuration.observability.add_log_channel(channel);
390            }
391            if let Some(channel) = loaded_observability_backend.get_metric_channel() {
392              server_configuration.observability.add_metric_channel(channel);
393            }
394            if let Some(channel) = loaded_observability_backend.get_trace_channel() {
395              server_configuration.observability.add_trace_channel(channel);
396            }
397          }
398          Err(error) => {
399            // Store the first error encountered
400            if first_server_module_error.is_none() {
401              first_server_module_error
402                .replace(anyhow::anyhow!("{error} (at {})", server_configuration.filters).into_boxed_dyn_error());
403            }
404            // Skip remaining observability backends for this configuration if loading fails
405            break;
406          }
407        }
408      }
409    }
410
411    if first_server_module_error.is_none() {
412      // Process each available server module
413      for server_module in server_modules.iter_mut() {
414        // Get module requirements
415        let requirements = server_module.get_requirements();
416        // Check if this module's requirements are satisfied by this configuration
417        let mut requirements_met = true;
418        for requirement in requirements {
419          requirements_met = false;
420          // Check if the required property exists and has a non-null value
421          if server_configuration
422            .entries
423            .get(requirement)
424            .and_then(|e| e.get_value())
425            .is_some_and(|v| !v.is_null() && v.as_bool().unwrap_or(true))
426          {
427            requirements_met = true;
428            break;
429          }
430        }
431        // Validate the configuration against this module
432        match server_module.validate_configuration(&server_configuration, &mut used_properties) {
433          Ok(_) => (),
434          Err(error) => {
435            // Store the first error encountered
436            if first_server_module_error.is_none() {
437              first_server_module_error
438                .replace(anyhow::anyhow!("{error} (at {})", server_configuration.filters).into_boxed_dyn_error());
439            }
440            // Skip remaining modules for this configuration if validation fails
441            break;
442          }
443        }
444        // Only load module if its requirements are met
445        if requirements_met {
446          // Load the module with current configuration and global configuration
447          match server_module.load_module(&server_configuration, global_configuration.as_ref(), secondary_runtime) {
448            Ok(loaded_module) => server_configuration.modules.push(loaded_module),
449            Err(error) => {
450              // Store the first error encountered
451              if first_server_module_error.is_none() {
452                first_server_module_error
453                  .replace(anyhow::anyhow!("{error} (at {})", server_configuration.filters).into_boxed_dyn_error());
454              }
455              // Skip remaining modules for this configuration if loading fails
456              break;
457            }
458          }
459        }
460      }
461    }
462
463    // Track unused properties (except for undocumented ones)
464    for property in server_configuration.entries.keys() {
465      if !property.starts_with("UNDOCUMENTED_") && !used_properties.contains(property) {
466        unused_properties.insert(property.to_string());
467      }
468    }
469
470    // Add the configuration with loaded modules to the result list
471    new_server_configurations.push(server_configuration);
472  }
473  // Return:
474  // 1. Server configurations with modules loaded
475  // 2. First error encountered (if any)
476  // 3. List of unused properties
477  (
478    new_server_configurations,
479    first_server_module_error,
480    unused_properties.into_iter().collect(),
481  )
482}
483
484/// Finds the global server configuration (host or non-host) from the given list of server configurations.
485fn find_global_configuration(server_configurations: &[ServerConfiguration]) -> Option<ServerConfiguration> {
486  // The server configurations are pre-merged, so we can simply return the found global configuration
487  let mut iterator = server_configurations.iter();
488  let first_found = iterator.find(|server_configuration| {
489    server_configuration.filters.is_global() || server_configuration.filters.is_global_non_host()
490  });
491  if let Some(first_found) = first_found {
492    if first_found.filters.is_global() {
493      return Some(first_found.clone());
494    }
495    for server_configuration in iterator {
496      if server_configuration.filters.is_global() {
497        return Some(server_configuration.clone());
498      } else if !server_configuration.filters.is_global_non_host() {
499        return Some(first_found.clone());
500      }
501    }
502  }
503  None
504}
505
506#[cfg(test)]
507mod tests {
508  use crate::config::*;
509
510  use super::*;
511  use std::collections::HashMap;
512  use std::net::{IpAddr, Ipv4Addr};
513
514  fn make_filters(
515    is_host: bool,
516    hostname: Option<&str>,
517    ip: Option<IpAddr>,
518    port: Option<u16>,
519    location_prefix: Option<&str>,
520    error_handler_status: Option<ErrorHandlerStatus>,
521  ) -> ServerConfigurationFilters {
522    ServerConfigurationFilters {
523      is_host,
524      hostname: hostname.map(String::from),
525      ip,
526      port,
527      condition: location_prefix.map(|prefix| Conditions {
528        location_prefix: prefix.to_string(),
529        conditionals: vec![],
530      }),
531      error_handler_status,
532    }
533  }
534
535  fn make_entry(values: Vec<ServerConfigurationValue>) -> ServerConfigurationEntries {
536    ServerConfigurationEntries {
537      inner: vec![ServerConfigurationEntry {
538        values,
539        props: HashMap::new(),
540      }],
541    }
542  }
543
544  fn make_entry_premerge(key: &str, value: ServerConfigurationValue) -> (String, ServerConfigurationEntries) {
545    let entry = ServerConfigurationEntry {
546      values: vec![value],
547      props: HashMap::new(),
548    };
549    (key.to_string(), ServerConfigurationEntries { inner: vec![entry] })
550  }
551
552  fn config_with_filters(
553    is_host: bool,
554    hostname: Option<&str>,
555    ip: Option<IpAddr>,
556    port: Option<u16>,
557    location_prefix: Option<&str>,
558    error_handler_status: Option<ErrorHandlerStatus>,
559    entries: Vec<(String, ServerConfigurationEntries)>,
560  ) -> ServerConfiguration {
561    ServerConfiguration {
562      filters: ServerConfigurationFilters {
563        is_host,
564        hostname: hostname.map(|s| s.to_string()),
565        ip,
566        port,
567        condition: location_prefix.map(|prefix| Conditions {
568          location_prefix: prefix.to_string(),
569          conditionals: vec![],
570        }),
571        error_handler_status,
572      },
573      entries: entries.into_iter().collect(),
574      modules: vec![],
575      observability: ObservabilityBackendChannels::new(),
576    }
577  }
578
579  #[test]
580  fn merges_identical_filters_and_combines_entries() {
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      Some("/api"),
587      Some(ErrorHandlerStatus::Status(404)),
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      Some("/api"),
596      Some(ErrorHandlerStatus::Status(404)),
597    );
598
599    let mut config1_entries = HashMap::new();
600    config1_entries.insert(
601      "route".to_string(),
602      make_entry(vec![ServerConfigurationValue::String("v1".to_string())]),
603    );
604
605    let mut config2_entries = HashMap::new();
606    config2_entries.insert(
607      "route".to_string(),
608      make_entry(vec![ServerConfigurationValue::String("v2".to_string())]),
609    );
610
611    let config1 = ServerConfiguration {
612      filters: filters_2,
613      entries: config1_entries,
614      modules: vec![],
615      observability: ObservabilityBackendChannels::new(),
616    };
617
618    let config2 = ServerConfiguration {
619      filters,
620      entries: config2_entries,
621      modules: vec![],
622      observability: ObservabilityBackendChannels::new(),
623    };
624
625    let merged = merge_duplicates(vec![config1, config2]);
626    assert_eq!(merged.len(), 1);
627
628    let merged_entries = &merged[0].entries;
629    assert!(merged_entries.contains_key("route"));
630    let route_entry = merged_entries.get("route").unwrap();
631    let values: Vec<_> = route_entry.inner.iter().flat_map(|e| e.values.iter()).collect();
632    assert_eq!(values.len(), 2);
633    assert!(values.contains(&&ServerConfigurationValue::String("v1".into())));
634    assert!(values.contains(&&ServerConfigurationValue::String("v2".into())));
635  }
636
637  #[test]
638  fn does_not_merge_different_filters() {
639    let filters1 = make_filters(
640      true,
641      Some("example.com"),
642      Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
643      Some(8080),
644      Some("/api"),
645      None,
646    );
647
648    let filters2 = make_filters(
649      true,
650      Some("example.org"),
651      Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
652      Some(8080),
653      Some("/api"),
654      None,
655    );
656
657    let mut config1_entries = HashMap::new();
658    config1_entries.insert(
659      "route".to_string(),
660      make_entry(vec![ServerConfigurationValue::String("v1".to_string())]),
661    );
662
663    let mut config2_entries = HashMap::new();
664    config2_entries.insert(
665      "route".to_string(),
666      make_entry(vec![ServerConfigurationValue::String("v2".to_string())]),
667    );
668
669    let config1 = ServerConfiguration {
670      filters: filters1,
671      entries: config1_entries,
672      modules: vec![],
673      observability: ObservabilityBackendChannels::new(),
674    };
675
676    let config2 = ServerConfiguration {
677      filters: filters2,
678      entries: config2_entries,
679      modules: vec![],
680      observability: ObservabilityBackendChannels::new(),
681    };
682
683    let merged = merge_duplicates(vec![config1, config2]);
684    assert_eq!(merged.len(), 2);
685  }
686
687  #[test]
688  fn handles_filters_then_unique_then_duplicate() {
689    let filters1 = make_filters(
690      true,
691      Some("example.com"),
692      Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
693      Some(8080),
694      Some("/api"),
695      None,
696    );
697
698    let filters2 = make_filters(
699      true,
700      Some("example.org"),
701      Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
702      Some(8080),
703      Some("/api"),
704      None,
705    );
706
707    let mut config1_entries = HashMap::new();
708    config1_entries.insert(
709      "route".to_string(),
710      make_entry(vec![ServerConfigurationValue::String("v1".to_string())]),
711    );
712
713    let mut config2_entries = HashMap::new();
714    config2_entries.insert(
715      "route".to_string(),
716      make_entry(vec![ServerConfigurationValue::String("v2".to_string())]),
717    );
718
719    let mut config3_entries = HashMap::new();
720    config3_entries.insert(
721      "route".to_string(),
722      make_entry(vec![ServerConfigurationValue::String("v3".to_string())]),
723    );
724
725    let config1 = ServerConfiguration {
726      filters: filters1.clone(),
727      entries: config1_entries,
728      modules: vec![],
729      observability: ObservabilityBackendChannels::new(),
730    };
731
732    let config2 = ServerConfiguration {
733      filters: filters2,
734      entries: config2_entries,
735      modules: vec![],
736      observability: ObservabilityBackendChannels::new(),
737    };
738
739    let config3 = ServerConfiguration {
740      filters: filters1,
741      entries: config3_entries,
742      modules: vec![],
743      observability: ObservabilityBackendChannels::new(),
744    };
745
746    let merged = merge_duplicates(vec![config1, config2, config3]);
747    assert_eq!(merged.len(), 2);
748  }
749
750  #[test]
751  fn merges_entries_with_non_overlapping_keys() {
752    let filters = make_filters(
753      true,
754      Some("example.com"),
755      Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
756      Some(8080),
757      None,
758      None,
759    );
760
761    let filters_2 = make_filters(
762      true,
763      Some("example.com"),
764      Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
765      Some(8080),
766      None,
767      None,
768    );
769
770    let mut config1_entries = HashMap::new();
771    config1_entries.insert(
772      "route1".to_string(),
773      make_entry(vec![ServerConfigurationValue::String("r1".to_string())]),
774    );
775
776    let mut config2_entries = HashMap::new();
777    config2_entries.insert(
778      "route2".to_string(),
779      make_entry(vec![ServerConfigurationValue::String("r2".to_string())]),
780    );
781
782    let config1 = ServerConfiguration {
783      filters: filters_2,
784      entries: config1_entries,
785      modules: vec![],
786      observability: ObservabilityBackendChannels::new(),
787    };
788
789    let config2 = ServerConfiguration {
790      filters,
791      entries: config2_entries,
792      modules: vec![],
793      observability: ObservabilityBackendChannels::new(),
794    };
795
796    let merged = merge_duplicates(vec![config1, config2]);
797    assert_eq!(merged.len(), 1);
798
799    let merged_entries = &merged[0].entries;
800    assert_eq!(merged_entries.len(), 2);
801    assert!(merged_entries.contains_key("route1"));
802    assert!(merged_entries.contains_key("route2"));
803  }
804
805  #[test]
806  fn test_no_merge_returns_all() {
807    let config1 = config_with_filters(
808      true,
809      Some("example.com"),
810      Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
811      Some(80),
812      None,
813      None,
814      vec![make_entry_premerge(
815        "key1",
816        ServerConfigurationValue::String("val1".into()),
817      )],
818    );
819
820    let config2 = config_with_filters(
821      true,
822      Some("example.org"),
823      Some(IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1))),
824      Some(8080),
825      None,
826      None,
827      vec![make_entry_premerge(
828        "key2",
829        ServerConfigurationValue::String("val2".into()),
830      )],
831    );
832
833    let merged = premerge_configuration(vec![config1, config2]);
834
835    assert_eq!(merged.len(), 2);
836    assert!(merged.iter().any(|c| c.entries.contains_key("key1")));
837    assert!(merged.iter().any(|c| c.entries.contains_key("key2")));
838  }
839
840  #[test]
841  fn test_merge_case6_is_host() {
842    // Less specific config (no port)
843    let base = config_with_filters(
844      false,
845      None,
846      None,
847      None,
848      None,
849      None,
850      vec![make_entry_premerge(
851        "shared",
852        ServerConfigurationValue::String("base".into()),
853      )],
854    );
855
856    // More specific config (with port)
857    let specific = config_with_filters(
858      true,
859      None,
860      None,
861      None,
862      None,
863      None,
864      vec![make_entry_premerge(
865        "shared",
866        ServerConfigurationValue::String("specific".into()),
867      )],
868    );
869
870    let merged = premerge_configuration(vec![base, specific]);
871    assert_eq!(merged.len(), 2);
872
873    let entries = &merged[1].entries["shared"].inner;
874    assert_eq!(entries.len(), 1);
875    assert_eq!(entries[0].values[0].as_str(), Some("specific"));
876  }
877
878  #[test]
879  fn test_merge_case5_port() {
880    // Less specific config (no port)
881    let base = config_with_filters(
882      true,
883      None,
884      None,
885      None,
886      None,
887      None,
888      vec![make_entry_premerge(
889        "shared",
890        ServerConfigurationValue::String("base".into()),
891      )],
892    );
893
894    // More specific config (with port)
895    let specific = config_with_filters(
896      true,
897      None,
898      None,
899      Some(80),
900      None,
901      None,
902      vec![make_entry_premerge(
903        "shared",
904        ServerConfigurationValue::String("specific".into()),
905      )],
906    );
907
908    let merged = premerge_configuration(vec![base, specific]);
909    assert_eq!(merged.len(), 2);
910
911    let entries = &merged[1].entries["shared"].inner;
912    assert_eq!(entries.len(), 1);
913    assert_eq!(entries[0].values[0].as_str(), Some("specific"));
914  }
915
916  #[test]
917  fn test_merge_case1_error_handler() {
918    let base = config_with_filters(
919      true,
920      Some("host"),
921      Some(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4))),
922      Some(3000),
923      Some("/api"),
924      None,
925      vec![make_entry_premerge(
926        "eh",
927        ServerConfigurationValue::String("base".into()),
928      )],
929    );
930
931    let specific = config_with_filters(
932      true,
933      Some("host"),
934      Some(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4))),
935      Some(3000),
936      Some("/api"),
937      Some(ErrorHandlerStatus::Any),
938      vec![make_entry_premerge(
939        "eh",
940        ServerConfigurationValue::String("specific".into()),
941      )],
942    );
943
944    let merged = premerge_configuration(vec![base, specific]);
945    assert_eq!(merged.len(), 2);
946
947    let entries = &merged[1].entries["eh"].inner;
948    assert_eq!(entries.len(), 1);
949    assert_eq!(entries[0].values[0].as_str(), Some("specific"));
950  }
951
952  #[test]
953  fn test_merge_preserves_specificity_order() {
954    let configs = vec![
955      config_with_filters(
956        true,
957        None,
958        None,
959        None,
960        None,
961        None,
962        vec![make_entry_premerge("a", ServerConfigurationValue::String("v1".into()))],
963      ),
964      config_with_filters(
965        true,
966        None,
967        None,
968        Some(80),
969        None,
970        None,
971        vec![make_entry_premerge("a", ServerConfigurationValue::String("v2".into()))],
972      ),
973      config_with_filters(
974        true,
975        Some("host"),
976        None,
977        Some(80),
978        None,
979        None,
980        vec![make_entry_premerge("a", ServerConfigurationValue::String("v3".into()))],
981      ),
982    ];
983
984    let merged = premerge_configuration(configs);
985    assert_eq!(merged.len(), 3);
986
987    let entries = &merged[2].entries["a"].inner;
988    assert_eq!(entries.len(), 1);
989    assert_eq!(entries[0].values[0].as_str(), Some("v3"));
990  }
991}