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      if let Err(error) =
368        server_observability_backend.validate_configuration(&server_configuration, &mut used_properties)
369      {
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      // Only load observability backend if its requirements are met
379      if requirements_met {
380        // Load the observability backend with current configuration and global configuration
381        match server_observability_backend.load_observability_backend(
382          &server_configuration,
383          global_configuration.as_ref(),
384          secondary_runtime,
385        ) {
386          Ok(loaded_observability_backend) => {
387            if let Some(channel) = loaded_observability_backend.get_log_channel() {
388              server_configuration.observability.add_log_channel(channel);
389            }
390            if let Some(channel) = loaded_observability_backend.get_metric_channel() {
391              server_configuration.observability.add_metric_channel(channel);
392            }
393            if let Some(channel) = loaded_observability_backend.get_trace_channel() {
394              server_configuration.observability.add_trace_channel(channel);
395            }
396          }
397          Err(error) => {
398            // Store the first error encountered
399            if first_server_module_error.is_none() {
400              first_server_module_error
401                .replace(anyhow::anyhow!("{error} (at {})", server_configuration.filters).into_boxed_dyn_error());
402            }
403            // Skip remaining observability backends for this configuration if loading fails
404            break;
405          }
406        }
407      }
408    }
409
410    if first_server_module_error.is_none() {
411      // Process each available server module
412      for server_module in server_modules.iter_mut() {
413        // Get module requirements
414        let requirements = server_module.get_requirements();
415        // Check if this module's requirements are satisfied by this configuration
416        let mut requirements_met = true;
417        for requirement in requirements {
418          requirements_met = false;
419          // Check if the required property exists and has a non-null value
420          if server_configuration
421            .entries
422            .get(requirement)
423            .and_then(|e| e.get_value())
424            .is_some_and(|v| !v.is_null() && v.as_bool().unwrap_or(true))
425          {
426            requirements_met = true;
427            break;
428          }
429        }
430        // Validate the configuration against this module
431        if let Err(error) = server_module.validate_configuration(&server_configuration, &mut used_properties) {
432          // Store the first error encountered
433          if first_server_module_error.is_none() {
434            first_server_module_error
435              .replace(anyhow::anyhow!("{error} (at {})", server_configuration.filters).into_boxed_dyn_error());
436          }
437          // Skip remaining modules for this configuration if validation fails
438          break;
439        }
440        // Only load module if its requirements are met
441        if requirements_met {
442          // Load the module with current configuration and global configuration
443          match server_module.load_module(&server_configuration, global_configuration.as_ref(), secondary_runtime) {
444            Ok(loaded_module) => server_configuration.modules.push(loaded_module),
445            Err(error) => {
446              // Store the first error encountered
447              if first_server_module_error.is_none() {
448                first_server_module_error
449                  .replace(anyhow::anyhow!("{error} (at {})", server_configuration.filters).into_boxed_dyn_error());
450              }
451              // Skip remaining modules for this configuration if loading fails
452              break;
453            }
454          }
455        }
456      }
457    }
458
459    // Track unused properties (except for undocumented ones)
460    for property in server_configuration.entries.keys() {
461      if !property.starts_with("UNDOCUMENTED_") && !used_properties.contains(property) {
462        unused_properties.insert(property.to_string());
463      }
464    }
465
466    // Add the configuration with loaded modules to the result list
467    new_server_configurations.push(server_configuration);
468  }
469  // Return:
470  // 1. Server configurations with modules loaded
471  // 2. First error encountered (if any)
472  // 3. List of unused properties
473  (
474    new_server_configurations,
475    first_server_module_error,
476    unused_properties.into_iter().collect(),
477  )
478}
479
480/// Finds the global server configuration (host or non-host) from the given list of server configurations.
481fn find_global_configuration(server_configurations: &[ServerConfiguration]) -> Option<ServerConfiguration> {
482  // The server configurations are pre-merged, so we can simply return the found global configuration
483  let mut iterator = server_configurations.iter();
484  let first_found = iterator.find(|server_configuration| {
485    server_configuration.filters.is_global() || server_configuration.filters.is_global_non_host()
486  });
487  if let Some(first_found) = first_found {
488    if first_found.filters.is_global() {
489      return Some(first_found.clone());
490    }
491    for server_configuration in iterator {
492      if server_configuration.filters.is_global() {
493        return Some(server_configuration.clone());
494      } else if !server_configuration.filters.is_global_non_host() {
495        return Some(first_found.clone());
496      }
497    }
498  }
499  None
500}
501
502#[cfg(test)]
503mod tests {
504  use crate::config::*;
505
506  use super::*;
507  use std::collections::HashMap;
508  use std::net::{IpAddr, Ipv4Addr};
509
510  fn make_filters(
511    is_host: bool,
512    hostname: Option<&str>,
513    ip: Option<IpAddr>,
514    port: Option<u16>,
515    location_prefix: Option<&str>,
516    error_handler_status: Option<ErrorHandlerStatus>,
517  ) -> ServerConfigurationFilters {
518    ServerConfigurationFilters {
519      is_host,
520      hostname: hostname.map(String::from),
521      ip,
522      port,
523      condition: location_prefix.map(|prefix| Conditions {
524        location_prefix: prefix.to_string(),
525        conditionals: vec![],
526      }),
527      error_handler_status,
528    }
529  }
530
531  fn make_entry(values: Vec<ServerConfigurationValue>) -> ServerConfigurationEntries {
532    ServerConfigurationEntries {
533      inner: vec![ServerConfigurationEntry {
534        values,
535        props: HashMap::new(),
536      }],
537    }
538  }
539
540  fn make_entry_premerge(key: &str, value: ServerConfigurationValue) -> (String, ServerConfigurationEntries) {
541    let entry = ServerConfigurationEntry {
542      values: vec![value],
543      props: HashMap::new(),
544    };
545    (key.to_string(), ServerConfigurationEntries { inner: vec![entry] })
546  }
547
548  fn config_with_filters(
549    is_host: bool,
550    hostname: Option<&str>,
551    ip: Option<IpAddr>,
552    port: Option<u16>,
553    location_prefix: Option<&str>,
554    error_handler_status: Option<ErrorHandlerStatus>,
555    entries: Vec<(String, ServerConfigurationEntries)>,
556  ) -> ServerConfiguration {
557    ServerConfiguration {
558      filters: ServerConfigurationFilters {
559        is_host,
560        hostname: hostname.map(|s| s.to_string()),
561        ip,
562        port,
563        condition: location_prefix.map(|prefix| Conditions {
564          location_prefix: prefix.to_string(),
565          conditionals: vec![],
566        }),
567        error_handler_status,
568      },
569      entries: entries.into_iter().collect(),
570      modules: vec![],
571      observability: ObservabilityBackendChannels::new(),
572    }
573  }
574
575  #[test]
576  fn merges_identical_filters_and_combines_entries() {
577    let filters = make_filters(
578      true,
579      Some("example.com"),
580      Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
581      Some(8080),
582      Some("/api"),
583      Some(ErrorHandlerStatus::Status(404)),
584    );
585
586    let filters_2 = make_filters(
587      true,
588      Some("example.com"),
589      Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
590      Some(8080),
591      Some("/api"),
592      Some(ErrorHandlerStatus::Status(404)),
593    );
594
595    let mut config1_entries = HashMap::new();
596    config1_entries.insert(
597      "route".to_string(),
598      make_entry(vec![ServerConfigurationValue::String("v1".to_string())]),
599    );
600
601    let mut config2_entries = HashMap::new();
602    config2_entries.insert(
603      "route".to_string(),
604      make_entry(vec![ServerConfigurationValue::String("v2".to_string())]),
605    );
606
607    let config1 = ServerConfiguration {
608      filters: filters_2,
609      entries: config1_entries,
610      modules: vec![],
611      observability: ObservabilityBackendChannels::new(),
612    };
613
614    let config2 = ServerConfiguration {
615      filters,
616      entries: config2_entries,
617      modules: vec![],
618      observability: ObservabilityBackendChannels::new(),
619    };
620
621    let merged = merge_duplicates(vec![config1, config2]);
622    assert_eq!(merged.len(), 1);
623
624    let merged_entries = &merged[0].entries;
625    assert!(merged_entries.contains_key("route"));
626    let route_entry = merged_entries.get("route").unwrap();
627    let values: Vec<_> = route_entry.inner.iter().flat_map(|e| e.values.iter()).collect();
628    assert_eq!(values.len(), 2);
629    assert!(values.contains(&&ServerConfigurationValue::String("v1".into())));
630    assert!(values.contains(&&ServerConfigurationValue::String("v2".into())));
631  }
632
633  #[test]
634  fn does_not_merge_different_filters() {
635    let filters1 = make_filters(
636      true,
637      Some("example.com"),
638      Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
639      Some(8080),
640      Some("/api"),
641      None,
642    );
643
644    let filters2 = make_filters(
645      true,
646      Some("example.org"),
647      Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
648      Some(8080),
649      Some("/api"),
650      None,
651    );
652
653    let mut config1_entries = HashMap::new();
654    config1_entries.insert(
655      "route".to_string(),
656      make_entry(vec![ServerConfigurationValue::String("v1".to_string())]),
657    );
658
659    let mut config2_entries = HashMap::new();
660    config2_entries.insert(
661      "route".to_string(),
662      make_entry(vec![ServerConfigurationValue::String("v2".to_string())]),
663    );
664
665    let config1 = ServerConfiguration {
666      filters: filters1,
667      entries: config1_entries,
668      modules: vec![],
669      observability: ObservabilityBackendChannels::new(),
670    };
671
672    let config2 = ServerConfiguration {
673      filters: filters2,
674      entries: config2_entries,
675      modules: vec![],
676      observability: ObservabilityBackendChannels::new(),
677    };
678
679    let merged = merge_duplicates(vec![config1, config2]);
680    assert_eq!(merged.len(), 2);
681  }
682
683  #[test]
684  fn handles_filters_then_unique_then_duplicate() {
685    let filters1 = make_filters(
686      true,
687      Some("example.com"),
688      Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
689      Some(8080),
690      Some("/api"),
691      None,
692    );
693
694    let filters2 = make_filters(
695      true,
696      Some("example.org"),
697      Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
698      Some(8080),
699      Some("/api"),
700      None,
701    );
702
703    let mut config1_entries = HashMap::new();
704    config1_entries.insert(
705      "route".to_string(),
706      make_entry(vec![ServerConfigurationValue::String("v1".to_string())]),
707    );
708
709    let mut config2_entries = HashMap::new();
710    config2_entries.insert(
711      "route".to_string(),
712      make_entry(vec![ServerConfigurationValue::String("v2".to_string())]),
713    );
714
715    let mut config3_entries = HashMap::new();
716    config3_entries.insert(
717      "route".to_string(),
718      make_entry(vec![ServerConfigurationValue::String("v3".to_string())]),
719    );
720
721    let config1 = ServerConfiguration {
722      filters: filters1.clone(),
723      entries: config1_entries,
724      modules: vec![],
725      observability: ObservabilityBackendChannels::new(),
726    };
727
728    let config2 = ServerConfiguration {
729      filters: filters2,
730      entries: config2_entries,
731      modules: vec![],
732      observability: ObservabilityBackendChannels::new(),
733    };
734
735    let config3 = ServerConfiguration {
736      filters: filters1,
737      entries: config3_entries,
738      modules: vec![],
739      observability: ObservabilityBackendChannels::new(),
740    };
741
742    let merged = merge_duplicates(vec![config1, config2, config3]);
743    assert_eq!(merged.len(), 2);
744  }
745
746  #[test]
747  fn merges_entries_with_non_overlapping_keys() {
748    let filters = make_filters(
749      true,
750      Some("example.com"),
751      Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
752      Some(8080),
753      None,
754      None,
755    );
756
757    let filters_2 = make_filters(
758      true,
759      Some("example.com"),
760      Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
761      Some(8080),
762      None,
763      None,
764    );
765
766    let mut config1_entries = HashMap::new();
767    config1_entries.insert(
768      "route1".to_string(),
769      make_entry(vec![ServerConfigurationValue::String("r1".to_string())]),
770    );
771
772    let mut config2_entries = HashMap::new();
773    config2_entries.insert(
774      "route2".to_string(),
775      make_entry(vec![ServerConfigurationValue::String("r2".to_string())]),
776    );
777
778    let config1 = ServerConfiguration {
779      filters: filters_2,
780      entries: config1_entries,
781      modules: vec![],
782      observability: ObservabilityBackendChannels::new(),
783    };
784
785    let config2 = ServerConfiguration {
786      filters,
787      entries: config2_entries,
788      modules: vec![],
789      observability: ObservabilityBackendChannels::new(),
790    };
791
792    let merged = merge_duplicates(vec![config1, config2]);
793    assert_eq!(merged.len(), 1);
794
795    let merged_entries = &merged[0].entries;
796    assert_eq!(merged_entries.len(), 2);
797    assert!(merged_entries.contains_key("route1"));
798    assert!(merged_entries.contains_key("route2"));
799  }
800
801  #[test]
802  fn test_no_merge_returns_all() {
803    let config1 = config_with_filters(
804      true,
805      Some("example.com"),
806      Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
807      Some(80),
808      None,
809      None,
810      vec![make_entry_premerge(
811        "key1",
812        ServerConfigurationValue::String("val1".into()),
813      )],
814    );
815
816    let config2 = config_with_filters(
817      true,
818      Some("example.org"),
819      Some(IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1))),
820      Some(8080),
821      None,
822      None,
823      vec![make_entry_premerge(
824        "key2",
825        ServerConfigurationValue::String("val2".into()),
826      )],
827    );
828
829    let merged = premerge_configuration(vec![config1, config2]);
830
831    assert_eq!(merged.len(), 2);
832    assert!(merged.iter().any(|c| c.entries.contains_key("key1")));
833    assert!(merged.iter().any(|c| c.entries.contains_key("key2")));
834  }
835
836  #[test]
837  fn test_merge_case6_is_host() {
838    // Less specific config (no port)
839    let base = config_with_filters(
840      false,
841      None,
842      None,
843      None,
844      None,
845      None,
846      vec![make_entry_premerge(
847        "shared",
848        ServerConfigurationValue::String("base".into()),
849      )],
850    );
851
852    // More specific config (with port)
853    let specific = config_with_filters(
854      true,
855      None,
856      None,
857      None,
858      None,
859      None,
860      vec![make_entry_premerge(
861        "shared",
862        ServerConfigurationValue::String("specific".into()),
863      )],
864    );
865
866    let merged = premerge_configuration(vec![base, specific]);
867    assert_eq!(merged.len(), 2);
868
869    let entries = &merged[1].entries["shared"].inner;
870    assert_eq!(entries.len(), 1);
871    assert_eq!(entries[0].values[0].as_str(), Some("specific"));
872  }
873
874  #[test]
875  fn test_merge_case5_port() {
876    // Less specific config (no port)
877    let base = config_with_filters(
878      true,
879      None,
880      None,
881      None,
882      None,
883      None,
884      vec![make_entry_premerge(
885        "shared",
886        ServerConfigurationValue::String("base".into()),
887      )],
888    );
889
890    // More specific config (with port)
891    let specific = config_with_filters(
892      true,
893      None,
894      None,
895      Some(80),
896      None,
897      None,
898      vec![make_entry_premerge(
899        "shared",
900        ServerConfigurationValue::String("specific".into()),
901      )],
902    );
903
904    let merged = premerge_configuration(vec![base, specific]);
905    assert_eq!(merged.len(), 2);
906
907    let entries = &merged[1].entries["shared"].inner;
908    assert_eq!(entries.len(), 1);
909    assert_eq!(entries[0].values[0].as_str(), Some("specific"));
910  }
911
912  #[test]
913  fn test_merge_case1_error_handler() {
914    let base = config_with_filters(
915      true,
916      Some("host"),
917      Some(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4))),
918      Some(3000),
919      Some("/api"),
920      None,
921      vec![make_entry_premerge(
922        "eh",
923        ServerConfigurationValue::String("base".into()),
924      )],
925    );
926
927    let specific = config_with_filters(
928      true,
929      Some("host"),
930      Some(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4))),
931      Some(3000),
932      Some("/api"),
933      Some(ErrorHandlerStatus::Any),
934      vec![make_entry_premerge(
935        "eh",
936        ServerConfigurationValue::String("specific".into()),
937      )],
938    );
939
940    let merged = premerge_configuration(vec![base, specific]);
941    assert_eq!(merged.len(), 2);
942
943    let entries = &merged[1].entries["eh"].inner;
944    assert_eq!(entries.len(), 1);
945    assert_eq!(entries[0].values[0].as_str(), Some("specific"));
946  }
947
948  #[test]
949  fn test_merge_preserves_specificity_order() {
950    let configs = vec![
951      config_with_filters(
952        true,
953        None,
954        None,
955        None,
956        None,
957        None,
958        vec![make_entry_premerge("a", ServerConfigurationValue::String("v1".into()))],
959      ),
960      config_with_filters(
961        true,
962        None,
963        None,
964        Some(80),
965        None,
966        None,
967        vec![make_entry_premerge("a", ServerConfigurationValue::String("v2".into()))],
968      ),
969      config_with_filters(
970        true,
971        Some("host"),
972        None,
973        Some(80),
974        None,
975        None,
976        vec![make_entry_premerge("a", ServerConfigurationValue::String("v3".into()))],
977      ),
978    ];
979
980    let merged = premerge_configuration(configs);
981    assert_eq!(merged.len(), 3);
982
983    let entries = &merged[2].entries["a"].inner;
984    assert_eq!(entries.len(), 1);
985    assert_eq!(entries[0].values[0].as_str(), Some("v3"));
986  }
987}