Skip to main content

gobby_core/
context.rs

1//! Shared runtime context boundary.
2//!
3//! Consumer crates keep their CLI flags and domain state locally. This module
4//! owns the public location for cross-crate project, daemon, and service context
5//! types as the Rust foundation expands.
6
7use 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/// Resolved runtime context for any gobby-core consumer.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct CoreContext {
17    /// Project root directory containing `.gobby/`.
18    project_root: PathBuf,
19    /// Project ID from `.gobby/project.json`.
20    project_id: String,
21    /// PostgreSQL hub DSN resolved by the consumer.
22    database_url: Option<String>,
23    /// FalkorDB config when available.
24    falkordb: Option<FalkorConfig>,
25    /// Qdrant config when available.
26    qdrant: Option<QdrantConfig>,
27    /// Embedding API config when available.
28    embedding: Option<EmbeddingConfig>,
29    /// Gobby daemon base URL.
30    daemon_url: String,
31}
32
33impl CoreContext {
34    /// Build a context from pre-resolved project identity and DSN inputs.
35    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                // SAFETY: TEST_ENV_LOCK serializes all test environment mutation
117                // here, and the loop only touches the fixed key list above.
118                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}