use crate::user_config::helpers::resolve_record_setting;
use bytesize::ByteSize;
use serde::Deserialize;
use std::time::Duration;
use target_spec::{Platform, TargetSpec};
pub const MIN_MAX_OUTPUT_SIZE: ByteSize = ByteSize::b(1000);
pub const MAX_MAX_OUTPUT_SIZE: ByteSize = ByteSize::mib(256);
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct DeserializedRecordConfig {
#[serde(default)]
pub enabled: Option<bool>,
#[serde(default)]
pub max_records: Option<usize>,
#[serde(default)]
pub max_total_size: Option<ByteSize>,
#[serde(default, with = "humantime_serde")]
pub max_age: Option<Duration>,
#[serde(default)]
pub max_output_size: Option<ByteSize>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct DefaultRecordConfig {
pub enabled: bool,
pub max_records: usize,
pub max_total_size: ByteSize,
#[serde(with = "humantime_serde")]
pub max_age: Duration,
pub max_output_size: ByteSize,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub(in crate::user_config) struct DeserializedRecordOverrideData {
pub(in crate::user_config) enabled: Option<bool>,
pub(in crate::user_config) max_records: Option<usize>,
pub(in crate::user_config) max_total_size: Option<ByteSize>,
#[serde(default, with = "humantime_serde")]
pub(in crate::user_config) max_age: Option<Duration>,
pub(in crate::user_config) max_output_size: Option<ByteSize>,
}
#[derive(Clone, Debug)]
pub(in crate::user_config) struct CompiledRecordOverride {
platform_spec: TargetSpec,
data: RecordOverrideData,
}
impl CompiledRecordOverride {
pub(in crate::user_config) fn new(
platform_spec: TargetSpec,
data: DeserializedRecordOverrideData,
) -> Self {
Self {
platform_spec,
data: RecordOverrideData {
enabled: data.enabled,
max_records: data.max_records,
max_total_size: data.max_total_size,
max_age: data.max_age,
max_output_size: data.max_output_size,
},
}
}
pub(in crate::user_config) fn matches(&self, host_platform: &Platform) -> bool {
self.platform_spec
.eval(host_platform)
.unwrap_or( false)
}
pub(in crate::user_config) fn data(&self) -> &RecordOverrideData {
&self.data
}
}
#[derive(Clone, Debug, Default)]
pub(in crate::user_config) struct RecordOverrideData {
enabled: Option<bool>,
max_records: Option<usize>,
max_total_size: Option<ByteSize>,
max_age: Option<Duration>,
max_output_size: Option<ByteSize>,
}
impl RecordOverrideData {
pub(in crate::user_config) fn enabled(&self) -> Option<&bool> {
self.enabled.as_ref()
}
pub(in crate::user_config) fn max_records(&self) -> Option<&usize> {
self.max_records.as_ref()
}
pub(in crate::user_config) fn max_total_size(&self) -> Option<&ByteSize> {
self.max_total_size.as_ref()
}
pub(in crate::user_config) fn max_age(&self) -> Option<&Duration> {
self.max_age.as_ref()
}
pub(in crate::user_config) fn max_output_size(&self) -> Option<&ByteSize> {
self.max_output_size.as_ref()
}
}
#[derive(Clone, Debug)]
pub struct RecordConfig {
pub enabled: bool,
pub max_records: usize,
pub max_total_size: ByteSize,
pub max_age: Duration,
pub max_output_size: ByteSize,
}
impl RecordConfig {
pub(in crate::user_config) fn resolve(
default_config: &DefaultRecordConfig,
default_overrides: &[CompiledRecordOverride],
user_config: Option<&DeserializedRecordConfig>,
user_overrides: &[CompiledRecordOverride],
host_platform: &Platform,
) -> Self {
let mut max_output_size = resolve_record_setting(
&default_config.max_output_size,
default_overrides,
user_config.and_then(|c| c.max_output_size.as_ref()),
user_overrides,
host_platform,
|data| data.max_output_size(),
);
if max_output_size < MIN_MAX_OUTPUT_SIZE {
tracing::warn!(
"max-output-size ({}) is below minimum ({}), using minimum",
max_output_size,
MIN_MAX_OUTPUT_SIZE,
);
max_output_size = MIN_MAX_OUTPUT_SIZE;
} else if max_output_size > MAX_MAX_OUTPUT_SIZE {
tracing::warn!(
"max-output-size ({}) exceeds maximum ({}), using maximum",
max_output_size,
MAX_MAX_OUTPUT_SIZE,
);
max_output_size = MAX_MAX_OUTPUT_SIZE;
}
Self {
enabled: resolve_record_setting(
&default_config.enabled,
default_overrides,
user_config.and_then(|c| c.enabled.as_ref()),
user_overrides,
host_platform,
|data| data.enabled(),
),
max_records: resolve_record_setting(
&default_config.max_records,
default_overrides,
user_config.and_then(|c| c.max_records.as_ref()),
user_overrides,
host_platform,
|data| data.max_records(),
),
max_total_size: resolve_record_setting(
&default_config.max_total_size,
default_overrides,
user_config.and_then(|c| c.max_total_size.as_ref()),
user_overrides,
host_platform,
|data| data.max_total_size(),
),
max_age: resolve_record_setting(
&default_config.max_age,
default_overrides,
user_config.and_then(|c| c.max_age.as_ref()),
user_overrides,
host_platform,
|data| data.max_age(),
),
max_output_size,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::platform::detect_host_platform_for_tests;
#[test]
fn test_deserialized_record_config_parsing() {
let config: DeserializedRecordConfig = toml::from_str(
r#"
enabled = true
max-records = 50
max-total-size = "2GB"
max-age = "7d"
max-output-size = "5MB"
"#,
)
.unwrap();
assert_eq!(config.enabled, Some(true));
assert_eq!(config.max_records, Some(50));
assert_eq!(config.max_total_size, Some(ByteSize::gb(2)));
assert_eq!(config.max_age, Some(Duration::from_secs(7 * 24 * 60 * 60)));
assert_eq!(config.max_output_size, Some(ByteSize::mb(5)));
let config: DeserializedRecordConfig = toml::from_str(
r#"
max-records = 100
"#,
)
.unwrap();
assert!(config.enabled.is_none());
assert_eq!(config.max_records, Some(100));
assert!(config.max_total_size.is_none());
assert!(config.max_age.is_none());
assert!(config.max_output_size.is_none());
let config: DeserializedRecordConfig = toml::from_str("").unwrap();
assert!(config.enabled.is_none());
assert!(config.max_records.is_none());
assert!(config.max_total_size.is_none());
assert!(config.max_age.is_none());
assert!(config.max_output_size.is_none());
}
#[test]
fn test_default_record_config_parsing() {
let config: DefaultRecordConfig = toml::from_str(
r#"
enabled = true
max-records = 100
max-total-size = "1GB"
max-age = "30d"
max-output-size = "10MB"
"#,
)
.unwrap();
assert!(config.enabled);
assert_eq!(config.max_records, 100);
assert_eq!(config.max_total_size, ByteSize::gb(1));
assert_eq!(config.max_age, Duration::from_secs(30 * 24 * 60 * 60));
assert_eq!(config.max_output_size, ByteSize::mb(10));
}
#[test]
fn test_resolve_uses_defaults() {
let defaults = DefaultRecordConfig {
enabled: false,
max_records: 100,
max_total_size: ByteSize::gb(1),
max_age: Duration::from_secs(30 * 24 * 60 * 60),
max_output_size: ByteSize::mb(10),
};
let host = detect_host_platform_for_tests();
let resolved = RecordConfig::resolve(&defaults, &[], None, &[], &host);
assert!(!resolved.enabled);
assert_eq!(resolved.max_records, 100);
assert_eq!(resolved.max_total_size, ByteSize::gb(1));
assert_eq!(resolved.max_age, Duration::from_secs(30 * 24 * 60 * 60));
assert_eq!(resolved.max_output_size, ByteSize::mb(10));
}
#[test]
fn test_resolve_user_overrides_defaults() {
let defaults = DefaultRecordConfig {
enabled: false,
max_records: 100,
max_total_size: ByteSize::gb(1),
max_age: Duration::from_secs(30 * 24 * 60 * 60),
max_output_size: ByteSize::mb(10),
};
let user_config = DeserializedRecordConfig {
enabled: Some(true),
max_records: Some(50),
max_total_size: None,
max_age: Some(Duration::from_secs(7 * 24 * 60 * 60)),
max_output_size: Some(ByteSize::mb(5)),
};
let host = detect_host_platform_for_tests();
let resolved = RecordConfig::resolve(&defaults, &[], Some(&user_config), &[], &host);
assert!(resolved.enabled); assert_eq!(resolved.max_records, 50); assert_eq!(resolved.max_total_size, ByteSize::gb(1)); assert_eq!(resolved.max_age, Duration::from_secs(7 * 24 * 60 * 60)); assert_eq!(resolved.max_output_size, ByteSize::mb(5)); }
#[test]
fn test_resolve_clamps_small_max_output_size() {
let defaults = DefaultRecordConfig {
enabled: false,
max_records: 100,
max_total_size: ByteSize::gb(1),
max_age: Duration::from_secs(30 * 24 * 60 * 60),
max_output_size: ByteSize::mb(10),
};
let user_config = DeserializedRecordConfig {
enabled: None,
max_records: None,
max_total_size: None,
max_age: None,
max_output_size: Some(ByteSize::b(100)), };
let host = detect_host_platform_for_tests();
let resolved = RecordConfig::resolve(&defaults, &[], Some(&user_config), &[], &host);
assert_eq!(resolved.max_output_size, MIN_MAX_OUTPUT_SIZE);
}
#[test]
fn test_resolve_accepts_value_at_minimum() {
let defaults = DefaultRecordConfig {
enabled: false,
max_records: 100,
max_total_size: ByteSize::gb(1),
max_age: Duration::from_secs(30 * 24 * 60 * 60),
max_output_size: ByteSize::mb(10),
};
let user_config = DeserializedRecordConfig {
enabled: None,
max_records: None,
max_total_size: None,
max_age: None,
max_output_size: Some(MIN_MAX_OUTPUT_SIZE),
};
let host = detect_host_platform_for_tests();
let resolved = RecordConfig::resolve(&defaults, &[], Some(&user_config), &[], &host);
assert_eq!(resolved.max_output_size, MIN_MAX_OUTPUT_SIZE);
}
#[test]
fn test_resolve_clamps_large_max_output_size() {
let defaults = DefaultRecordConfig {
enabled: false,
max_records: 100,
max_total_size: ByteSize::gb(1),
max_age: Duration::from_secs(30 * 24 * 60 * 60),
max_output_size: ByteSize::mb(10),
};
let user_config = DeserializedRecordConfig {
enabled: None,
max_records: None,
max_total_size: None,
max_age: None,
max_output_size: Some(ByteSize::gib(1)), };
let host = detect_host_platform_for_tests();
let resolved = RecordConfig::resolve(&defaults, &[], Some(&user_config), &[], &host);
assert_eq!(resolved.max_output_size, MAX_MAX_OUTPUT_SIZE);
}
#[test]
fn test_resolve_accepts_value_at_maximum() {
let defaults = DefaultRecordConfig {
enabled: false,
max_records: 100,
max_total_size: ByteSize::gb(1),
max_age: Duration::from_secs(30 * 24 * 60 * 60),
max_output_size: ByteSize::mb(10),
};
let user_config = DeserializedRecordConfig {
enabled: None,
max_records: None,
max_total_size: None,
max_age: None,
max_output_size: Some(MAX_MAX_OUTPUT_SIZE),
};
let host = detect_host_platform_for_tests();
let resolved = RecordConfig::resolve(&defaults, &[], Some(&user_config), &[], &host);
assert_eq!(resolved.max_output_size, MAX_MAX_OUTPUT_SIZE);
}
fn make_override(
platform: &str,
data: DeserializedRecordOverrideData,
) -> CompiledRecordOverride {
let platform_spec =
TargetSpec::new(platform.to_string()).expect("valid platform spec in test");
CompiledRecordOverride::new(platform_spec, data)
}
#[test]
fn test_resolve_user_override_applies() {
let defaults = DefaultRecordConfig {
enabled: false,
max_records: 100,
max_total_size: ByteSize::gb(1),
max_age: Duration::from_secs(30 * 24 * 60 * 60),
max_output_size: ByteSize::mb(10),
};
let override_ = make_override(
"cfg(all())",
DeserializedRecordOverrideData {
enabled: Some(true),
max_records: Some(50),
..Default::default()
},
);
let host = detect_host_platform_for_tests();
let resolved = RecordConfig::resolve(&defaults, &[], None, &[override_], &host);
assert!(resolved.enabled);
assert_eq!(resolved.max_records, 50);
assert_eq!(resolved.max_total_size, ByteSize::gb(1)); assert_eq!(resolved.max_age, Duration::from_secs(30 * 24 * 60 * 60)); assert_eq!(resolved.max_output_size, ByteSize::mb(10)); }
#[test]
fn test_resolve_default_override_applies() {
let defaults = DefaultRecordConfig {
enabled: false,
max_records: 100,
max_total_size: ByteSize::gb(1),
max_age: Duration::from_secs(30 * 24 * 60 * 60),
max_output_size: ByteSize::mb(10),
};
let override_ = make_override(
"cfg(all())",
DeserializedRecordOverrideData {
enabled: Some(true),
max_records: Some(50),
..Default::default()
},
);
let host = detect_host_platform_for_tests();
let resolved = RecordConfig::resolve(&defaults, &[override_], None, &[], &host);
assert!(resolved.enabled);
assert_eq!(resolved.max_records, 50);
assert_eq!(resolved.max_total_size, ByteSize::gb(1)); }
#[test]
fn test_resolve_platform_override_no_match() {
let defaults = DefaultRecordConfig {
enabled: false,
max_records: 100,
max_total_size: ByteSize::gb(1),
max_age: Duration::from_secs(30 * 24 * 60 * 60),
max_output_size: ByteSize::mb(10),
};
let override_ = make_override(
"cfg(any())",
DeserializedRecordOverrideData {
enabled: Some(true),
max_records: Some(50),
max_total_size: Some(ByteSize::gb(2)),
max_age: Some(Duration::from_secs(7 * 24 * 60 * 60)),
max_output_size: Some(ByteSize::mb(5)),
},
);
let host = detect_host_platform_for_tests();
let resolved = RecordConfig::resolve(&defaults, &[], None, &[override_], &host);
assert!(!resolved.enabled);
assert_eq!(resolved.max_records, 100);
assert_eq!(resolved.max_total_size, ByteSize::gb(1));
assert_eq!(resolved.max_age, Duration::from_secs(30 * 24 * 60 * 60));
assert_eq!(resolved.max_output_size, ByteSize::mb(10));
}
#[test]
fn test_resolve_first_matching_user_override_wins() {
let defaults = DefaultRecordConfig {
enabled: false,
max_records: 100,
max_total_size: ByteSize::gb(1),
max_age: Duration::from_secs(30 * 24 * 60 * 60),
max_output_size: ByteSize::mb(10),
};
let override1 = make_override(
"cfg(all())",
DeserializedRecordOverrideData {
enabled: Some(true),
..Default::default()
},
);
let override2 = make_override(
"cfg(all())",
DeserializedRecordOverrideData {
enabled: Some(false), max_records: Some(50),
..Default::default()
},
);
let host = detect_host_platform_for_tests();
let resolved = RecordConfig::resolve(&defaults, &[], None, &[override1, override2], &host);
assert!(resolved.enabled);
assert_eq!(resolved.max_records, 50);
}
#[test]
fn test_resolve_user_override_beats_default_override() {
let defaults = DefaultRecordConfig {
enabled: false,
max_records: 100,
max_total_size: ByteSize::gb(1),
max_age: Duration::from_secs(30 * 24 * 60 * 60),
max_output_size: ByteSize::mb(10),
};
let user_override = make_override(
"cfg(all())",
DeserializedRecordOverrideData {
enabled: Some(true),
..Default::default()
},
);
let default_override = make_override(
"cfg(all())",
DeserializedRecordOverrideData {
enabled: Some(false), max_records: Some(50),
..Default::default()
},
);
let host = detect_host_platform_for_tests();
let resolved = RecordConfig::resolve(
&defaults,
&[default_override],
None,
&[user_override],
&host,
);
assert!(resolved.enabled);
assert_eq!(resolved.max_records, 50);
}
#[test]
fn test_resolve_override_beats_user_base() {
let defaults = DefaultRecordConfig {
enabled: false,
max_records: 100,
max_total_size: ByteSize::gb(1),
max_age: Duration::from_secs(30 * 24 * 60 * 60),
max_output_size: ByteSize::mb(10),
};
let user_config = DeserializedRecordConfig {
enabled: Some(false),
max_records: Some(25),
..Default::default()
};
let default_override = make_override(
"cfg(all())",
DeserializedRecordOverrideData {
enabled: Some(true),
..Default::default()
},
);
let host = detect_host_platform_for_tests();
let resolved = RecordConfig::resolve(
&defaults,
&[default_override],
Some(&user_config),
&[],
&host,
);
assert!(resolved.enabled);
assert_eq!(resolved.max_records, 25);
}
#[test]
fn test_resolve_override_clamps_max_output_size() {
let defaults = DefaultRecordConfig {
enabled: false,
max_records: 100,
max_total_size: ByteSize::gb(1),
max_age: Duration::from_secs(30 * 24 * 60 * 60),
max_output_size: ByteSize::mb(10),
};
let override_ = make_override(
"cfg(all())",
DeserializedRecordOverrideData {
max_output_size: Some(ByteSize::b(100)), ..Default::default()
},
);
let host = detect_host_platform_for_tests();
let resolved = RecordConfig::resolve(&defaults, &[], None, &[override_], &host);
assert_eq!(resolved.max_output_size, MIN_MAX_OUTPUT_SIZE);
}
}