use serde::Deserialize;
use tracing::warn;
use super::RetrievalMode;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct SourceConfig {
pub enabled: Option<bool>,
pub mode: RetrievalMode,
}
impl SourceConfig {
pub fn effective_enabled(&self, creds_present: bool) -> bool {
self.enabled.unwrap_or(creds_present)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ContextSourcesConfig {
pub jira: SourceConfig,
pub confluence: SourceConfig,
pub github_issues: SourceConfig,
pub conformance: SourceConfig,
}
impl ContextSourcesConfig {
pub fn from_env_and_file(file: Option<&ContextSourcesFileConfig>) -> Self {
Self {
jira: resolve_source("JIRA", file.map(|f| &f.jira)),
confluence: resolve_source("CONFLUENCE", file.map(|f| &f.confluence)),
github_issues: resolve_source("GITHUB_ISSUES", file.map(|f| &f.github_issues)),
conformance: resolve_source("CONFORMANCE", file.map(|f| &f.conformance)),
}
}
}
fn resolve_source(env_key: &str, file: Option<&SourceFileConfig>) -> SourceConfig {
let mut cfg = SourceConfig {
enabled: file.and_then(|f| f.enabled),
mode: file.and_then(|f| f.mode).unwrap_or_default(),
};
if let Some(v) = parse_bool_env(&format!("TRUSTY_REVIEW_CONTEXT_{env_key}_ENABLED")) {
cfg.enabled = Some(v);
}
if let Some(m) = parse_mode_env(&format!("TRUSTY_REVIEW_CONTEXT_{env_key}_MODE")) {
cfg.mode = m;
}
cfg
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct ContextSourcesFileConfig {
#[serde(default)]
pub jira: SourceFileConfig,
#[serde(default)]
pub confluence: SourceFileConfig,
#[serde(default)]
pub github_issues: SourceFileConfig,
#[serde(default)]
pub conformance: SourceFileConfig,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct SourceFileConfig {
pub enabled: Option<bool>,
pub mode: Option<RetrievalMode>,
}
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() {
"true" | "1" | "yes" | "on" => Some(true),
"false" | "0" | "no" | "off" => Some(false),
other => {
warn!("unrecognised boolean for {var}: {other:?} — ignoring");
None
}
}
}
fn parse_mode_env(var: &str) -> Option<RetrievalMode> {
let raw = std::env::var(var).ok()?;
let v = raw.trim().to_lowercase();
if v.is_empty() {
return None;
}
match v.as_str() {
"live" => Some(RetrievalMode::Live),
"semantic" | "indexed" => Some(RetrievalMode::Semantic),
other => {
warn!("unrecognised retrieval mode for {var}: {other:?} — ignoring");
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
fn clear_env() {
unsafe {
for k in [
"TRUSTY_REVIEW_CONTEXT_JIRA_ENABLED",
"TRUSTY_REVIEW_CONTEXT_JIRA_MODE",
"TRUSTY_REVIEW_CONTEXT_CONFLUENCE_ENABLED",
"TRUSTY_REVIEW_CONTEXT_CONFLUENCE_MODE",
"TRUSTY_REVIEW_CONTEXT_GITHUB_ISSUES_ENABLED",
"TRUSTY_REVIEW_CONTEXT_GITHUB_ISSUES_MODE",
] {
std::env::remove_var(k);
}
}
}
#[test]
fn source_defaults_disabled_without_creds() {
let cfg = SourceConfig::default();
assert!(!cfg.effective_enabled(false));
assert!(cfg.effective_enabled(true));
assert_eq!(cfg.mode, RetrievalMode::Live);
}
#[test]
fn effective_enabled_honours_explicit_false_even_with_creds() {
let cfg = SourceConfig {
enabled: Some(false),
mode: RetrievalMode::Live,
};
assert!(!cfg.effective_enabled(true));
}
#[test]
fn effective_enabled_honours_explicit_true_without_creds() {
let cfg = SourceConfig {
enabled: Some(true),
mode: RetrievalMode::Live,
};
assert!(cfg.effective_enabled(false));
}
#[test]
#[serial]
fn from_env_and_file_defaults_disabled() {
clear_env();
let cfg = ContextSourcesConfig::from_env_and_file(None);
assert_eq!(cfg.jira.enabled, None);
assert_eq!(cfg.confluence.enabled, None);
assert_eq!(cfg.github_issues.enabled, None);
assert_eq!(cfg.jira.mode, RetrievalMode::Live);
clear_env();
}
#[test]
#[serial]
fn env_enables_source() {
clear_env();
unsafe {
std::env::set_var("TRUSTY_REVIEW_CONTEXT_JIRA_ENABLED", "true");
}
let cfg = ContextSourcesConfig::from_env_and_file(None);
assert_eq!(cfg.jira.enabled, Some(true));
assert_eq!(cfg.confluence.enabled, None);
clear_env();
}
#[test]
#[serial]
fn file_sets_mode() {
clear_env();
let file = ContextSourcesFileConfig {
jira: SourceFileConfig {
enabled: Some(true),
mode: Some(RetrievalMode::Semantic),
},
..Default::default()
};
let cfg = ContextSourcesConfig::from_env_and_file(Some(&file));
assert_eq!(cfg.jira.enabled, Some(true));
assert_eq!(cfg.jira.mode, RetrievalMode::Semantic);
clear_env();
}
#[test]
#[serial]
fn env_beats_file() {
clear_env();
unsafe {
std::env::set_var("TRUSTY_REVIEW_CONTEXT_JIRA_ENABLED", "false");
std::env::set_var("TRUSTY_REVIEW_CONTEXT_JIRA_MODE", "live");
}
let file = ContextSourcesFileConfig {
jira: SourceFileConfig {
enabled: Some(true),
mode: Some(RetrievalMode::Semantic),
},
..Default::default()
};
let cfg = ContextSourcesConfig::from_env_and_file(Some(&file));
assert_eq!(cfg.jira.enabled, Some(false));
assert_eq!(cfg.jira.mode, RetrievalMode::Live);
clear_env();
}
#[test]
#[serial]
fn mode_parses() {
clear_env();
unsafe {
std::env::set_var("TRUSTY_REVIEW_CONTEXT_GITHUB_ISSUES_MODE", "semantic");
}
let cfg = ContextSourcesConfig::from_env_and_file(None);
assert_eq!(cfg.github_issues.mode, RetrievalMode::Semantic);
clear_env();
}
#[test]
#[serial]
fn unrecognised_env_falls_through() {
clear_env();
unsafe {
std::env::set_var("TRUSTY_REVIEW_CONTEXT_JIRA_ENABLED", "maybe");
std::env::set_var("TRUSTY_REVIEW_CONTEXT_JIRA_MODE", "fuzzy");
}
let cfg = ContextSourcesConfig::from_env_and_file(None);
assert_eq!(cfg.jira.enabled, None, "garbage enabled ignored");
assert_eq!(cfg.jira.mode, RetrievalMode::Live, "garbage mode ignored");
clear_env();
}
}