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 "GOBBY_EMBEDDING_URL",
116 "GOBBY_EMBEDDING_MODEL",
117 "GOBBY_EMBEDDING_API_KEY",
118 ] {
119 unsafe { std::env::remove_var(key) };
122 }
123 }
124
125 fn set(&self, key: &str, value: &str) {
126 unsafe { std::env::set_var(key, value) };
127 }
128 }
129
130 impl Drop for EnvGuard {
131 fn drop(&mut self) {
132 self.clear();
133 }
134 }
135
136 struct TestConfigSource {
137 values: HashMap<&'static str, String>,
138 }
139
140 impl TestConfigSource {
141 fn with_values(values: impl IntoIterator<Item = (&'static str, &'static str)>) -> Self {
142 Self {
143 values: values
144 .into_iter()
145 .map(|(key, value)| (key, value.to_string()))
146 .collect(),
147 }
148 }
149 }
150
151 impl ConfigSource for TestConfigSource {
152 fn config_value(&mut self, key: &str) -> Option<String> {
153 self.values.get(key).cloned()
154 }
155
156 fn resolve_value(&mut self, value: &str) -> anyhow::Result<String> {
157 Ok(value.to_string())
158 }
159 }
160
161 #[test]
162 fn missing_optional_services_are_none() {
163 let _env = EnvGuard::new();
164 let mut source = EnvOnlySource;
165 let root = std::path::PathBuf::from("/tmp/gobby-project");
166
167 let context = CoreContext::build(root.clone(), "project-id".to_string(), None, &mut source);
168
169 assert_eq!(context.project_root(), root.as_path());
170 assert_eq!(context.project_id(), "project-id");
171 assert_eq!(context.database_url(), None);
172 assert!(context.falkordb().is_none());
173 assert!(context.qdrant().is_none());
174 assert!(context.embedding().is_none());
175 assert!(!context.daemon_url().is_empty());
176 }
177
178 #[test]
179 fn build_with_env_only_source() {
180 let env = EnvGuard::new();
181 env.set("GOBBY_FALKORDB_HOST", "env-falkor.local");
182 env.set("GOBBY_FALKORDB_PORT", "17000");
183 env.set("GOBBY_QDRANT_URL", "http://env-qdrant:6333");
184 env.set("GOBBY_EMBEDDING_URL", "http://env-embedding:11434");
185 env.set("GOBBY_EMBEDDING_MODEL", "env-model");
186
187 let mut source = EnvOnlySource;
188 let root = std::path::PathBuf::from("/tmp/gobby-project");
189
190 let context = CoreContext::build(
191 root.clone(),
192 "project-id".to_string(),
193 Some("postgres://example".to_string()),
194 &mut source,
195 );
196
197 assert_eq!(context.project_root(), root.as_path());
198 assert_eq!(context.project_id(), "project-id");
199 assert_eq!(context.database_url(), Some("postgres://example"));
200 assert_eq!(
201 context.falkordb().map(|c| c.host.as_str()),
202 Some("env-falkor.local")
203 );
204 assert_eq!(
205 context.qdrant().and_then(|c| c.url.as_deref()),
206 Some("http://env-qdrant:6333")
207 );
208 assert!(context.embedding().is_none());
209 assert!(!context.daemon_url().is_empty());
210 }
211
212 #[test]
213 fn build_with_config_source_embedding() {
214 let env = EnvGuard::new();
215 env.set("GOBBY_EMBEDDING_URL", "http://env-embedding:11434");
216 env.set("GOBBY_EMBEDDING_MODEL", "env-model");
217
218 let mut source = TestConfigSource::with_values([
219 (embedding_keys::AI_API_BASE, "http://config-embedding:11434"),
220 (embedding_keys::AI_MODEL, "config-model"),
221 ]);
222 let root = std::path::PathBuf::from("/tmp/gobby-project");
223
224 let context = CoreContext::build(root, "project-id".to_string(), None, &mut source);
225
226 let embedding = context.embedding().expect("embedding config");
227 assert_eq!(embedding.api_base, "http://config-embedding:11434");
228 assert_eq!(embedding.model, "config-model");
229 }
230}