1use serde::{Deserialize, Serialize};
7
8use crate::alignment::AlignmentConfig;
9use crate::lfq::LfqConfig;
10
11mod align;
12
13pub use align::AlignOutputConfig;
14pub use koth_core::config::*;
15
16#[derive(Debug, Clone, Serialize, Deserialize, Default)]
18#[serde(default, deny_unknown_fields)]
19pub struct AlignConfig {
20 pub alignment: AlignmentConfig,
21 pub lfq: LfqConfig,
22 pub output: AlignOutputConfig,
23}
24
25impl AlignConfig {
26 pub fn from_toml(path: &std::path::Path) -> Result<Self, crate::error::KothError> {
27 let content = std::fs::read_to_string(path)?;
28 let cfg: Self = toml::from_str(&content)
29 .map_err(|e| crate::error::KothError::ConfigError(e.to_string()))?;
30 cfg.lfq.consensus.validate()?;
31 Ok(cfg)
32 }
33
34 pub fn to_toml_string(&self) -> Result<String, crate::error::KothError> {
35 toml::to_string(self).map_err(|e| crate::error::KothError::ConfigError(e.to_string()))
36 }
37}
38
39#[cfg(test)]
40mod config_parse_tests {
41 use super::AlignConfig;
42
43 fn parse_align(name: &str, toml_src: &str) {
44 if let Err(e) = toml::from_str::<AlignConfig>(toml_src) {
45 panic!("shipped koth_align config `{name}` failed to parse: {e}");
46 }
47 }
48
49 #[test]
60 fn shipped_align_configs_parse() {
61 parse_align(
62 "example_config_align.toml",
63 include_str!("../../../example_config_align.toml"),
64 );
65 }
66
67 #[test]
68 fn removed_consensus_gates_are_rejected_instead_of_ignored() {
69 for (key, value) in [
70 ("min_member_combined_score", "0.5"),
71 ("min_seed_combined_score", "0.75"),
72 ("min_group_size", "2"),
73 ("allow_replicated_weak_seeds", "true"),
74 ] {
75 let source = format!("[lfq.consensus]\n{key} = {value}\n");
76 let err = toml::from_str::<AlignConfig>(&source).unwrap_err();
77 assert!(err.to_string().contains(key));
78 }
79 }
80
81 #[test]
82 fn consensus_gate_rejects_invalid_values() {
83 let mut cfg = AlignConfig::default();
84 for value in [f64::NAN, f64::INFINITY, -0.01, 1.01] {
85 cfg.lfq.consensus.max_group_qvalue = value;
86 assert!(cfg.lfq.consensus.validate().is_err());
87 }
88 cfg.lfq.consensus.max_group_qvalue = 0.05;
89 cfg.lfq.consensus.rt_window_pct = -0.1;
90 assert!(cfg.lfq.consensus.validate().is_err());
91 }
92
93 #[test]
94 fn omitted_lfq_windows_use_release_defaults_without_changing_grouping() {
95 for text in [
96 "",
97 "[lfq]\n",
98 include_str!("../../../example_config_align.toml"),
99 ] {
100 let cfg: AlignConfig = toml::from_str(text).expect("valid alignment config");
101 assert_eq!(cfg.lfq.rt_window_pct, 0.005);
102 assert_eq!(cfg.lfq.im_tolerance, 0.015);
103 assert_eq!(cfg.lfq.consensus.rt_window_pct, 0.02);
104 assert_eq!(cfg.lfq.consensus.im_tolerance, 0.05);
105 assert_eq!(cfg.alignment.im_tolerance, 0.05);
106 }
107 }
108
109 #[test]
110 fn explicit_lfq_windows_override_defaults_without_changing_grouping() {
111 let cfg: AlignConfig =
112 toml::from_str("[lfq]\nrt_window_pct = 0.01\nim_tolerance = 0.025\n")
113 .expect("valid custom extraction windows");
114 assert_eq!(cfg.lfq.rt_window_pct, 0.01);
115 assert_eq!(cfg.lfq.im_tolerance, 0.025);
116 assert_eq!(cfg.lfq.consensus.rt_window_pct, 0.02);
117 assert_eq!(cfg.lfq.consensus.im_tolerance, 0.05);
118 }
119}