use std::time::Duration;
use crate::config::schema::{
AiChat, AiConfig, AiEmbedding, AiProvider, AzureConfig, Config, ContainerLayout, CosmosConfig,
IngestInstance, InstanceConfig, InstanceSpec, McpInstance, SearchConfig, SourceAuth,
SourceConnection, SourceType,
};
pub fn template_for(name: &str) -> anyhow::Result<Config> {
match name {
"minimal" => Ok(minimal_template()),
"multi-source" => Ok(multi_source_template()),
"distributed" => Ok(distributed_template()),
other => anyhow::bail!(
"unknown template '{}'. Available: minimal, multi-source, distributed",
other
),
}
}
pub const BUILTIN_TEMPLATES: &[&str] = &["minimal", "multi-source", "distributed"];
pub fn minimal_template() -> Config {
Config {
azure: AzureConfig {
cosmos: CosmosConfig {
subscription_id: Some("${AZURE_SUBSCRIPTION_ID}".to_string()),
resource_group: Some("rg-quelch-prod".to_string()),
account: Some("my-cosmos".to_string()),
endpoint: "https://my-cosmos.documents.azure.com".to_string(),
database: "quelch".to_string(),
containers: ContainerLayout::default(),
meta_container: "quelch-meta".to_string(),
},
search: Some(SearchConfig {
endpoint: "https://my-search.search.windows.net".to_string(),
}),
ai: Some(AiConfig {
provider: AiProvider::Foundry,
endpoint: "https://my-foundry.cognitiveservices.azure.com".to_string(),
embedding: AiEmbedding {
deployment: "text-embedding-3-large".to_string(),
dimensions: 3072,
},
chat: AiChat {
deployment: "gpt-5-mini".to_string(),
model_name: "gpt-5-mini".to_string(),
},
}),
},
source_connections: vec![SourceConnection {
name: "jira-cloud".to_string(),
source_type: SourceType::Jira,
base_url: "https://your-org.atlassian.net".to_string(),
auth: SourceAuth::Basic {
email: "${ATLASSIAN_CLOUD_EMAIL}".to_string(),
token: "${ATLASSIAN_CLOUD_API_TOKEN}".to_string(),
},
projects: vec!["PROJ".to_string()],
spaces: vec![],
}],
instances: vec![
InstanceConfig {
name: "ingest-jira".to_string(),
spec: InstanceSpec::Ingest(IngestInstance {
connections: vec!["jira-cloud".to_string()],
cycle_interval: Duration::from_secs(5 * 60),
}),
},
InstanceConfig {
name: "mcp-prod".to_string(),
spec: InstanceSpec::Mcp(McpInstance {
expose: vec!["jira_issues".to_string()],
api_key: "${QUELCH_MCP_API_KEY}".to_string(),
knowledge_base: "quelch-prod-kb".to_string(),
listen: "0.0.0.0:8080".to_string(),
}),
},
],
}
}
pub fn multi_source_template() -> Config {
let mut base = minimal_template();
base.source_connections.push(SourceConnection {
name: "confluence-cloud".to_string(),
source_type: SourceType::Confluence,
base_url: "https://your-org.atlassian.net/wiki".to_string(),
auth: SourceAuth::Basic {
email: "${ATLASSIAN_CLOUD_EMAIL}".to_string(),
token: "${ATLASSIAN_CLOUD_API_TOKEN}".to_string(),
},
projects: vec![],
spaces: vec!["ENG".to_string()],
});
if let Some(inst) = base.instances.iter_mut().find(|i| i.name == "ingest-jira")
&& let InstanceSpec::Ingest(ref mut spec) = inst.spec
{
spec.connections.push("confluence-cloud".to_string());
}
if let Some(inst) = base.instances.iter_mut().find(|i| i.name == "mcp-prod")
&& let InstanceSpec::Mcp(ref mut spec) = inst.spec
{
spec.expose.push("confluence_pages".to_string());
}
base
}
pub fn distributed_template() -> Config {
let mut base = minimal_template();
base.source_connections[0] = SourceConnection {
name: "jira-dc".to_string(),
source_type: SourceType::Jira,
base_url: "https://jira.internal.example".to_string(),
auth: SourceAuth::Pat {
token: "${JIRA_DC_PAT}".to_string(),
},
projects: vec!["PROJ".to_string()],
spaces: vec![],
};
base.source_connections.push(SourceConnection {
name: "confluence-cloud".to_string(),
source_type: SourceType::Confluence,
base_url: "https://your-org.atlassian.net/wiki".to_string(),
auth: SourceAuth::Basic {
email: "${ATLASSIAN_CLOUD_EMAIL}".to_string(),
token: "${ATLASSIAN_CLOUD_API_TOKEN}".to_string(),
},
projects: vec![],
spaces: vec!["ENG".to_string()],
});
base.instances.retain(|i| i.name != "ingest-jira");
base.instances.insert(
0,
InstanceConfig {
name: "ingest-jira-dc".to_string(),
spec: InstanceSpec::Ingest(IngestInstance {
connections: vec!["jira-dc".to_string()],
cycle_interval: Duration::from_secs(5 * 60),
}),
},
);
base.instances.insert(
1,
InstanceConfig {
name: "ingest-confluence-cloud".to_string(),
spec: InstanceSpec::Ingest(IngestInstance {
connections: vec!["confluence-cloud".to_string()],
cycle_interval: Duration::from_secs(10 * 60),
}),
},
);
if let Some(inst) = base.instances.iter_mut().find(|i| i.name == "mcp-prod")
&& let InstanceSpec::Mcp(ref mut spec) = inst.spec
{
spec.expose.push("confluence_pages".to_string());
}
base
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::validate::validate;
fn round_trip(cfg: &Config) -> Config {
let yaml = serde_yaml::to_string(cfg).expect("template serializes");
serde_yaml::from_str(&yaml).expect("template re-parses")
}
#[test]
fn minimal_template_round_trips_and_validates() {
let cfg = minimal_template();
let again = round_trip(&cfg);
validate(&again).expect("minimal template validates");
assert_eq!(again.source_connections.len(), 1);
assert_eq!(again.instances.len(), 2);
}
#[test]
fn multi_source_template_round_trips_and_validates() {
let cfg = multi_source_template();
let again = round_trip(&cfg);
validate(&again).expect("multi-source template validates");
assert_eq!(again.source_connections.len(), 2);
let ingest = again
.instances
.iter()
.find(|i| i.name == "ingest-jira")
.expect("ingest-jira present");
let connections = match &ingest.spec {
InstanceSpec::Ingest(s) => &s.connections,
_ => panic!("not an ingest"),
};
assert_eq!(connections.len(), 2);
}
#[test]
fn distributed_template_round_trips_and_validates() {
let cfg = distributed_template();
let again = round_trip(&cfg);
validate(&again).expect("distributed template validates");
assert_eq!(again.source_connections.len(), 2);
let ingest_count = again
.instances
.iter()
.filter(|i| matches!(i.spec, InstanceSpec::Ingest(_)))
.count();
assert_eq!(ingest_count, 2);
}
#[test]
fn template_for_returns_known_templates() {
for name in BUILTIN_TEMPLATES {
template_for(name).unwrap_or_else(|_| panic!("template '{name}' missing"));
}
}
#[test]
fn template_for_unknown_returns_error() {
let err = template_for("does-not-exist").unwrap_err();
assert!(err.to_string().contains("does-not-exist"));
}
}