subcog/storage/context_template/
mod.rs1mod sqlite;
29mod traits;
30
31pub use sqlite::{ContextTemplateDbStats, SqliteContextTemplateStorage};
32pub use traits::ContextTemplateStorage;
33
34use crate::config::SubcogConfig;
35use crate::storage::index::DomainScope;
36use crate::{Error, Result};
37use std::path::PathBuf;
38use std::sync::Arc;
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
42pub enum ContextTemplateBackendType {
43 #[default]
45 Sqlite,
46}
47
48pub struct ContextTemplateStorageFactory;
50
51impl ContextTemplateStorageFactory {
52 pub fn create_for_scope(
71 scope: DomainScope,
72 config: &SubcogConfig,
73 ) -> Result<Arc<dyn ContextTemplateStorage>> {
74 if matches!(scope, DomainScope::Org)
75 && !(config.features.org_scope_enabled || cfg!(feature = "org-scope"))
76 {
77 return Err(Error::FeatureNotEnabled("org-scope".to_string()));
78 }
79
80 let path = match scope {
81 DomainScope::Project | DomainScope::User => config
82 .storage
83 .user
84 .path
85 .as_ref()
86 .map(PathBuf::from)
87 .or_else(SqliteContextTemplateStorage::default_user_path),
88 DomainScope::Org => {
89 let org = Self::resolve_org_identifier()?;
90 config
91 .storage
92 .org
93 .path
94 .as_ref()
95 .map(PathBuf::from)
96 .or_else(|| SqliteContextTemplateStorage::default_org_path(&org))
97 },
98 };
99
100 let db_path = path.ok_or_else(|| Error::OperationFailed {
101 operation: "create_context_template_storage".to_string(),
102 cause: "Could not determine database path".to_string(),
103 })?;
104
105 Ok(Arc::new(SqliteContextTemplateStorage::new(db_path)?))
106 }
107
108 pub fn create_with_path(path: PathBuf) -> Result<Arc<dyn ContextTemplateStorage>> {
118 Ok(Arc::new(SqliteContextTemplateStorage::new(path)?))
119 }
120
121 pub fn create_in_memory() -> Result<Arc<dyn ContextTemplateStorage>> {
127 Ok(Arc::new(SqliteContextTemplateStorage::in_memory()?))
128 }
129
130 fn resolve_org_identifier() -> Result<String> {
136 if let Ok(org) = std::env::var("SUBCOG_ORG")
138 && !org.is_empty()
139 {
140 return Ok(org);
141 }
142
143 if let Ok(cwd) = std::env::current_dir()
145 && let Some(org) = Self::extract_org_from_repo_path(&cwd)
146 {
147 return Ok(org);
148 }
149
150 Err(Error::InvalidInput(
151 "Could not resolve organization identifier. \
152 Set SUBCOG_ORG environment variable or ensure git remote is configured."
153 .to_string(),
154 ))
155 }
156
157 fn extract_org_from_repo_path(path: &std::path::Path) -> Option<String> {
159 let repo = git2::Repository::open(path).ok()?;
160 let remote = repo.find_remote("origin").ok()?;
161 let url = remote.url()?;
162 Self::extract_org_from_git_url(url)
163 }
164
165 fn extract_org_from_git_url(url: &str) -> Option<String> {
167 if let Some((rest, path_start)) = url
169 .strip_prefix("git@")
170 .and_then(|rest| rest.find(':').map(|path_start| (rest, path_start)))
171 {
172 let path = &rest[path_start + 1..];
173 return path.split('/').next().map(ToString::to_string);
174 }
175
176 let path = url
178 .strip_prefix("https://")
179 .or_else(|| url.strip_prefix("http://"))
180 .or_else(|| url.strip_prefix("ssh://"))
181 .or_else(|| url.strip_prefix("git://"));
182
183 if let Some(org) = path.and_then(|rest| rest.split('/').nth(1)) {
184 return Some(org.to_string());
185 }
186
187 None
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn test_context_template_backend_type_default() {
197 let default = ContextTemplateBackendType::default();
198 assert_eq!(default, ContextTemplateBackendType::Sqlite);
199 }
200
201 #[test]
202 fn test_create_in_memory() {
203 let storage = ContextTemplateStorageFactory::create_in_memory();
204 assert!(storage.is_ok());
205 }
206
207 #[test]
208 fn test_create_with_path() {
209 let dir = tempfile::TempDir::new().unwrap();
210 let db_path = dir.path().join("context_templates.db");
211
212 let storage = ContextTemplateStorageFactory::create_with_path(db_path);
213 assert!(storage.is_ok());
214 }
215
216 #[test]
217 fn test_extract_org_from_git_url() {
218 assert_eq!(
220 ContextTemplateStorageFactory::extract_org_from_git_url(
221 "https://github.com/zircote/subcog.git"
222 ),
223 Some("zircote".to_string())
224 );
225
226 assert_eq!(
228 ContextTemplateStorageFactory::extract_org_from_git_url(
229 "git@github.com:zircote/subcog.git"
230 ),
231 Some("zircote".to_string())
232 );
233
234 assert_eq!(
236 ContextTemplateStorageFactory::extract_org_from_git_url("not-a-url"),
237 None
238 );
239 }
240}