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                "GOBBY_EMBEDDING_URL",
116                "GOBBY_EMBEDDING_MODEL",
117                "GOBBY_EMBEDDING_API_KEY",
118            ] {
119                // SAFETY: TEST_ENV_LOCK serializes all test environment mutation
120                // here, and the loop only touches the fixed key list above.
121                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}