use std::sync::Arc;
use serde::{Deserialize, Serialize};
use crate::error::LogdrainError;
use crate::mask::Mask;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Options {
pub sim_threshold: f64,
pub depth: usize,
pub max_clusters_per_leaf: usize,
pub parametrize_numeric_tokens: bool,
pub wildcard: Arc<str>,
pub path_delimiters: Vec<char>,
pub first_line_path_delimiters: Vec<char>,
pub first_line_only: bool,
#[serde(skip)]
pub masks: Vec<Mask>,
}
impl Default for Options {
fn default() -> Self {
Options {
sim_threshold: 0.4,
depth: 4,
max_clusters_per_leaf: 100,
parametrize_numeric_tokens: true,
wildcard: Arc::from("<*>"),
path_delimiters: Vec::new(),
first_line_path_delimiters: Vec::new(),
first_line_only: false,
masks: Vec::new(),
}
}
}
impl Options {
pub(crate) fn prefix_len(&self) -> usize {
self.depth - 2
}
pub(crate) fn active_path_delimiters(&self) -> &[char] {
if self.first_line_only && !self.first_line_path_delimiters.is_empty() {
&self.first_line_path_delimiters
} else {
&self.path_delimiters
}
}
}
#[derive(Debug, Clone, Default)]
pub struct MinerBuilder {
opts: Options,
}
impl MinerBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn sim_threshold(mut self, v: f64) -> Self {
self.opts.sim_threshold = v;
self
}
pub fn depth(mut self, v: usize) -> Self {
self.opts.depth = v;
self
}
pub fn max_clusters_per_leaf(mut self, v: usize) -> Self {
self.opts.max_clusters_per_leaf = v;
self
}
pub fn parametrize_numeric_tokens(mut self, v: bool) -> Self {
self.opts.parametrize_numeric_tokens = v;
self
}
pub fn wildcard(mut self, v: &str) -> Self {
self.opts.wildcard = Arc::from(v);
self
}
pub fn path_delimiters(mut self, delims: &[char]) -> Self {
self.opts.path_delimiters = delims.to_vec();
self
}
pub fn first_line_path_delimiters(mut self, delims: &[char]) -> Self {
self.opts.first_line_path_delimiters = delims.to_vec();
self
}
pub fn first_line_only(mut self, v: bool) -> Self {
self.opts.first_line_only = v;
self
}
pub fn masks(mut self, masks: impl IntoIterator<Item = Mask>) -> Self {
self.opts.masks = masks.into_iter().collect();
self
}
pub fn build_options(self) -> Result<Options, LogdrainError> {
let o = &self.opts;
if o.depth < 2 {
return Err(LogdrainError::InvalidConfig(format!(
"depth must be >= 2, got {}",
o.depth
)));
}
if !(0.0..=1.0).contains(&o.sim_threshold) {
return Err(LogdrainError::InvalidConfig(format!(
"sim_threshold must be in [0.0, 1.0], got {}",
o.sim_threshold
)));
}
if o.max_clusters_per_leaf == 0 {
return Err(LogdrainError::InvalidConfig(
"max_clusters_per_leaf must be >= 1".into(),
));
}
Ok(self.opts)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults() {
let o = Options::default();
assert_eq!(o.sim_threshold, 0.4);
assert_eq!(o.depth, 4);
assert_eq!(o.max_clusters_per_leaf, 100);
assert!(o.parametrize_numeric_tokens);
assert_eq!(&*o.wildcard, "<*>");
assert!(o.path_delimiters.is_empty());
assert!(o.first_line_path_delimiters.is_empty());
assert!(!o.first_line_only);
assert!(o.masks.is_empty());
}
#[test]
fn builder_sets_all_fields() {
let o = MinerBuilder::new()
.sim_threshold(0.6)
.depth(5)
.max_clusters_per_leaf(50)
.parametrize_numeric_tokens(false)
.wildcard("<?>")
.path_delimiters(&['/'])
.first_line_path_delimiters(&['/', ':'])
.first_line_only(true)
.masks([crate::builtin_masks::uuid()])
.build_options()
.unwrap();
assert_eq!(o.sim_threshold, 0.6);
assert_eq!(o.depth, 5);
assert_eq!(o.max_clusters_per_leaf, 50);
assert!(!o.parametrize_numeric_tokens);
assert_eq!(&*o.wildcard, "<?>");
assert_eq!(o.path_delimiters, vec!['/']);
assert_eq!(o.first_line_path_delimiters, vec!['/', ':']);
assert!(o.first_line_only);
assert_eq!(o.masks.len(), 1);
}
#[test]
fn rejects_invalid_config() {
assert!(MinerBuilder::new().depth(1).build_options().is_err());
assert!(MinerBuilder::new()
.sim_threshold(1.5)
.build_options()
.is_err());
assert!(MinerBuilder::new()
.sim_threshold(-0.1)
.build_options()
.is_err());
assert!(MinerBuilder::new()
.max_clusters_per_leaf(0)
.build_options()
.is_err());
assert!(MinerBuilder::new().depth(2).build_options().is_ok());
}
#[test]
fn active_delimiters_prefer_first_line_when_enabled() {
let o = MinerBuilder::new()
.path_delimiters(&['/'])
.first_line_path_delimiters(&[':'])
.first_line_only(true)
.build_options()
.unwrap();
assert_eq!(o.active_path_delimiters(), &[':']);
let o2 = MinerBuilder::new()
.path_delimiters(&['/'])
.first_line_only(true)
.build_options()
.unwrap();
assert_eq!(o2.active_path_delimiters(), &['/']);
let o3 = MinerBuilder::new()
.path_delimiters(&['/'])
.first_line_path_delimiters(&[':'])
.build_options()
.unwrap();
assert_eq!(o3.active_path_delimiters(), &['/']);
}
}