use serde::Deserialize;
use tracing::warn;
use crate::integrations::context::ContextSourcesFileConfig;
const ENV_REQUIRE_SEARCH: &str = "TRUSTY_REVIEW_REQUIRE_SEARCH";
const ENV_REQUIRE_ANALYZE: &str = "TRUSTY_REVIEW_REQUIRE_ANALYZE";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContextConfig {
pub require_search: bool,
pub require_analyze: bool,
}
impl Default for ContextConfig {
fn default() -> Self {
Self {
require_search: true,
require_analyze: true,
}
}
}
impl ContextConfig {
pub fn from_env_and_file(file: Option<&ContextFileConfig>) -> Self {
let mut cfg = ContextConfig {
require_search: file.and_then(|f| f.require_search).unwrap_or(true),
require_analyze: file.and_then(|f| f.require_analyze).unwrap_or(true),
};
if let Some(v) = parse_bool_env(ENV_REQUIRE_SEARCH) {
cfg.require_search = v;
}
if let Some(v) = parse_bool_env(ENV_REQUIRE_ANALYZE) {
cfg.require_analyze = v;
}
cfg
}
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct ContextFileConfig {
pub require_search: Option<bool>,
pub require_analyze: Option<bool>,
#[serde(default)]
pub sources: ContextSourcesFileConfig,
}
fn parse_bool_env(var: &str) -> Option<bool> {
let raw = std::env::var(var).ok()?;
let v = raw.trim().to_lowercase();
if v.is_empty() {
return None;
}
match v.as_str() {
"false" | "0" | "no" | "off" => Some(false),
"true" | "1" | "yes" | "on" => Some(true),
other => {
warn!("unrecognised boolean for {var}: {other:?} — ignoring");
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
fn clear_env() {
unsafe {
std::env::remove_var(ENV_REQUIRE_SEARCH);
std::env::remove_var(ENV_REQUIRE_ANALYZE);
}
}
#[test]
fn context_defaults_required() {
let cfg = ContextConfig::default();
assert!(cfg.require_search, "search must default to REQUIRED");
assert!(cfg.require_analyze, "analyze must default to REQUIRED");
}
#[test]
#[serial]
fn context_env_relaxes_search() {
clear_env();
unsafe {
std::env::set_var(ENV_REQUIRE_SEARCH, "false");
}
let cfg = ContextConfig::from_env_and_file(None);
assert!(
!cfg.require_search,
"env false must relax search requirement"
);
assert!(cfg.require_analyze, "analyze untouched by search var");
clear_env();
}
#[test]
#[serial]
fn context_file_relaxes_analyze() {
clear_env();
let file = ContextFileConfig {
require_search: None,
require_analyze: Some(false),
..Default::default()
};
let cfg = ContextConfig::from_env_and_file(Some(&file));
assert!(cfg.require_search, "search stays required by default");
assert!(
!cfg.require_analyze,
"file false must relax analyze requirement"
);
clear_env();
}
#[test]
#[serial]
fn context_env_beats_file() {
clear_env();
unsafe {
std::env::set_var(ENV_REQUIRE_SEARCH, "true");
}
let file = ContextFileConfig {
require_search: Some(false),
require_analyze: None,
..Default::default()
};
let cfg = ContextConfig::from_env_and_file(Some(&file));
assert!(cfg.require_search, "env true must override file false");
clear_env();
}
#[test]
#[serial]
fn context_unrecognised_env_keeps_file_value() {
clear_env();
unsafe {
std::env::set_var(ENV_REQUIRE_ANALYZE, "maybe");
}
let file = ContextFileConfig {
require_search: None,
require_analyze: Some(false),
..Default::default()
};
let cfg = ContextConfig::from_env_and_file(Some(&file));
assert!(
!cfg.require_analyze,
"unrecognised env must fall through to file value"
);
clear_env();
}
}