1use std::path::{Path, PathBuf};
8
9use crate::config::{
10 ConfigSource, EmbeddingConfig, FalkorConfig, QdrantConfig, resolve_embedding_config,
11 resolve_falkordb_config, resolve_qdrant_config,
12};
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct CoreContext {
17 project_root: PathBuf,
19 project_id: String,
21 database_url: Option<String>,
23 falkordb: Option<FalkorConfig>,
25 qdrant: Option<QdrantConfig>,
27 embedding: Option<EmbeddingConfig>,
29 daemon_url: String,
31}
32
33impl CoreContext {
34 pub fn build(
36 project_root: PathBuf,
37 project_id: String,
38 database_url: Option<String>,
39 source: &mut impl ConfigSource,
40 ) -> Self {
41 let falkordb = resolve_falkordb_config(source);
42 let qdrant = resolve_qdrant_config(source);
43 let embedding = resolve_embedding_config(source);
44 let daemon_url = crate::daemon_url::daemon_url();
45
46 Self {
47 project_root,
48 project_id,
49 database_url,
50 falkordb,
51 qdrant,
52 embedding,
53 daemon_url,
54 }
55 }
56
57 pub fn project_root(&self) -> &Path {
58 &self.project_root
59 }
60
61 pub fn project_id(&self) -> &str {
62 &self.project_id
63 }
64
65 pub fn database_url(&self) -> Option<&str> {
66 self.database_url.as_deref()
67 }
68
69 pub fn falkordb(&self) -> Option<&FalkorConfig> {
70 self.falkordb.as_ref()
71 }
72
73 pub fn qdrant(&self) -> Option<&QdrantConfig> {
74 self.qdrant.as_ref()
75 }
76
77 pub fn embedding(&self) -> Option<&EmbeddingConfig> {
78 self.embedding.as_ref()
79 }
80
81 pub fn daemon_url(&self) -> &str {
82 &self.daemon_url
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89 use crate::config::{EnvOnlySource, TEST_ENV_LOCK, embedding_keys};
90 use std::collections::HashMap;
91 use std::sync::MutexGuard;
92
93 struct EnvGuard {
94 _lock: MutexGuard<'static, ()>,
95 }
96
97 impl EnvGuard {
98 fn new() -> Self {
99 let guard = Self {
100 _lock: TEST_ENV_LOCK
101 .lock()
102 .unwrap_or_else(|poisoned| poisoned.into_inner()),
103 };
104 guard.clear();
105 guard
106 }
107
108 fn clear(&self) {
109 for key in [
110 "GOBBY_FALKORDB_HOST",
111 "GOBBY_FALKORDB_PORT",
112 "GOBBY_FALKORDB_PASSWORD",
113 "GOBBY_QDRANT_URL",
114 "GOBBY_QDRANT_API_KEY",
115 ] {
116 unsafe { std::env::remove_var(key) };
119 }
120 }
121
122 fn set(&self, key: &str, value: &str) {
123 unsafe { std::env::set_var(key, value) };
124 }
125 }
126
127 impl Drop for EnvGuard {
128 fn drop(&mut self) {
129 self.clear();
130 }
131 }
132
133 struct TestConfigSource {
134 values: HashMap<&'static str, String>,
135 }
136
137 impl TestConfigSource {
138 fn with_values(values: impl IntoIterator<Item = (&'static str, &'static str)>) -> Self {
139 Self {
140 values: values
141 .into_iter()
142 .map(|(key, value)| (key, value.to_string()))
143 .collect(),
144 }
145 }
146 }
147
148 impl ConfigSource for TestConfigSource {
149 fn config_value(&mut self, key: &str) -> Option<String> {
150 self.values.get(key).cloned()
151 }
152
153 fn resolve_value(&mut self, value: &str) -> anyhow::Result<String> {
154 Ok(value.to_string())
155 }
156 }
157
158 #[test]
159 fn missing_diagnostic_service_config_is_none() {
160 let _env = EnvGuard::new();
161 let mut source = EnvOnlySource;
162 let root = std::path::PathBuf::from("/tmp/gobby-project");
163
164 let context = CoreContext::build(root.clone(), "project-id".to_string(), None, &mut source);
165
166 assert_eq!(context.project_root(), root.as_path());
167 assert_eq!(context.project_id(), "project-id");
168 assert_eq!(context.database_url(), None);
169 assert!(context.falkordb().is_none());
170 assert!(context.qdrant().is_none());
171 assert!(context.embedding().is_none());
172 assert!(!context.daemon_url().is_empty());
173 }
174
175 #[test]
176 fn build_with_env_only_source() {
177 let env = EnvGuard::new();
178 env.set("GOBBY_FALKORDB_HOST", "env-falkor.local");
179 env.set("GOBBY_FALKORDB_PORT", "17000");
180 env.set("GOBBY_QDRANT_URL", "http://env-qdrant:6333");
181
182 let mut source = EnvOnlySource;
183 let root = std::path::PathBuf::from("/tmp/gobby-project");
184
185 let context = CoreContext::build(
186 root.clone(),
187 "project-id".to_string(),
188 Some("postgres://example".to_string()),
189 &mut source,
190 );
191
192 assert_eq!(context.project_root(), root.as_path());
193 assert_eq!(context.project_id(), "project-id");
194 assert_eq!(context.database_url(), Some("postgres://example"));
195 assert_eq!(
196 context.falkordb().map(|c| c.host.as_str()),
197 Some("env-falkor.local")
198 );
199 assert_eq!(
200 context.qdrant().and_then(|c| c.url.as_deref()),
201 Some("http://env-qdrant:6333")
202 );
203 assert!(context.embedding().is_none());
204 assert!(!context.daemon_url().is_empty());
205 }
206
207 #[test]
208 fn build_with_config_source_embedding() {
209 let _env = EnvGuard::new();
210
211 let mut source = TestConfigSource::with_values([
212 (embedding_keys::AI_API_BASE, "http://config-embedding:11434"),
213 (embedding_keys::AI_MODEL, "config-model"),
214 ]);
215 let root = std::path::PathBuf::from("/tmp/gobby-project");
216
217 let context = CoreContext::build(root, "project-id".to_string(), None, &mut source);
218
219 let embedding = context.embedding().expect("embedding config");
220 assert_eq!(embedding.api_base, "http://config-embedding:11434");
221 assert_eq!(embedding.model, "config-model");
222 }
223}