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};
90    use std::sync::MutexGuard;
91
92    struct EnvGuard {
93        _lock: MutexGuard<'static, ()>,
94    }
95
96    impl EnvGuard {
97        fn new() -> Self {
98            let guard = Self {
99                _lock: TEST_ENV_LOCK
100                    .lock()
101                    .unwrap_or_else(|poisoned| poisoned.into_inner()),
102            };
103            guard.clear();
104            guard
105        }
106
107        fn clear(&self) {
108            for key in [
109                "GOBBY_FALKORDB_HOST",
110                "GOBBY_FALKORDB_PORT",
111                "GOBBY_FALKORDB_PASSWORD",
112                "GOBBY_QDRANT_URL",
113                "GOBBY_QDRANT_API_KEY",
114                "GOBBY_EMBEDDING_URL",
115                "GOBBY_EMBEDDING_MODEL",
116                "GOBBY_EMBEDDING_API_KEY",
117            ] {
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    #[test]
134    fn missing_optional_services_are_none() {
135        let _env = EnvGuard::new();
136        let mut source = EnvOnlySource;
137        let root = std::path::PathBuf::from("/tmp/gobby-project");
138
139        let context = CoreContext::build(root.clone(), "project-id".to_string(), None, &mut source);
140
141        assert_eq!(context.project_root(), root.as_path());
142        assert_eq!(context.project_id(), "project-id");
143        assert_eq!(context.database_url(), None);
144        assert!(context.falkordb().is_none());
145        assert!(context.qdrant().is_none());
146        assert!(context.embedding().is_none());
147        assert!(!context.daemon_url().is_empty());
148    }
149
150    #[test]
151    fn build_with_env_only_source() {
152        let env = EnvGuard::new();
153        env.set("GOBBY_FALKORDB_HOST", "env-falkor.local");
154        env.set("GOBBY_FALKORDB_PORT", "17000");
155        env.set("GOBBY_QDRANT_URL", "http://env-qdrant:6333");
156        env.set("GOBBY_EMBEDDING_URL", "http://env-embedding:11434");
157        env.set("GOBBY_EMBEDDING_MODEL", "env-model");
158
159        let mut source = EnvOnlySource;
160        let root = std::path::PathBuf::from("/tmp/gobby-project");
161
162        let context = CoreContext::build(
163            root.clone(),
164            "project-id".to_string(),
165            Some("postgres://example".to_string()),
166            &mut source,
167        );
168
169        assert_eq!(context.project_root(), root.as_path());
170        assert_eq!(context.project_id(), "project-id");
171        assert_eq!(context.database_url(), Some("postgres://example"));
172        assert_eq!(
173            context.falkordb().map(|c| c.host.as_str()),
174            Some("env-falkor.local")
175        );
176        assert_eq!(
177            context.qdrant().and_then(|c| c.url.as_deref()),
178            Some("http://env-qdrant:6333")
179        );
180        assert_eq!(
181            context.embedding().map(|c| c.api_base.as_str()),
182            Some("http://env-embedding:11434")
183        );
184        assert_eq!(
185            context.embedding().map(|c| c.model.as_str()),
186            Some("env-model")
187        );
188        assert!(!context.daemon_url().is_empty());
189    }
190}