sqlite_graphrag/namespace.rs
1//! Namespace resolution layer (flag > env > "global" fallback).
2//!
3//! Validates and resolves the active namespace used to scope all SQLite
4//! operations, enforcing safe characters and traversal-free names.
5
6use crate::errors::AppError;
7use crate::i18n::validation;
8use serde::Serialize;
9use std::path::Path;
10
11#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
12#[serde(rename_all = "snake_case")]
13pub enum NamespaceSource {
14 ExplicitFlag,
15 Environment,
16 Default,
17}
18
19#[derive(Debug, Clone, Serialize)]
20pub struct NamespaceResolution {
21 pub namespace: String,
22 pub source: NamespaceSource,
23 pub cwd: String,
24}
25
26/// Resolves the active namespace, returning only the final name.
27///
28/// Shortcut over [`detect_namespace`] when the source does not matter.
29/// With a valid explicit flag, the returned namespace is exactly the passed value.
30/// Without a flag, the final fallback is `"global"`.
31///
32/// # Errors
33///
34/// Returns [`AppError::Validation`] if `explicit` contains invalid characters
35/// or exceeds 80 characters.
36///
37/// # Examples
38///
39/// ```
40/// use sqlite_graphrag::namespace::resolve_namespace;
41///
42/// // A valid explicit flag is accepted and reflected in the result.
43/// let ns = resolve_namespace(Some("meu-projeto")).unwrap();
44/// assert_eq!(ns, "meu-projeto");
45/// ```
46///
47/// ```
48/// use sqlite_graphrag::namespace::resolve_namespace;
49/// use sqlite_graphrag::errors::AppError;
50///
51/// // Namespace with invalid characters causes a validation error (exit 1).
52/// let err = resolve_namespace(Some("ns with space")).unwrap_err();
53/// assert_eq!(err.exit_code(), 1);
54/// ```
55pub fn resolve_namespace(explicit: Option<&str>) -> Result<String, AppError> {
56 Ok(detect_namespace(explicit)?.namespace)
57}
58
59/// Resolves the active namespace, returning a struct with the source and current directory.
60///
61/// Precedence: explicit flag > `SQLITE_GRAPHRAG_NAMESPACE` > fallback `"global"`.
62///
63/// # Errors
64///
65/// Returns [`AppError::Validation`] if the resolved namespace contains invalid characters.
66///
67/// # Examples
68///
69/// ```
70/// use sqlite_graphrag::namespace::{detect_namespace, NamespaceSource};
71///
72/// // With an explicit flag, the source is `ExplicitFlag`.
73/// let res = detect_namespace(Some("producao")).unwrap();
74/// assert_eq!(res.namespace, "producao");
75/// assert_eq!(res.source, NamespaceSource::ExplicitFlag);
76/// ```
77///
78/// ```
79/// use sqlite_graphrag::namespace::{detect_namespace, NamespaceSource};
80///
81/// // Without any explicit configuration, fallback is "global".
82/// // Removes env var to guarantee deterministic behaviour.
83/// std::env::remove_var("SQLITE_GRAPHRAG_NAMESPACE");
84/// let res = detect_namespace(None).unwrap();
85/// assert_eq!(res.namespace, "global");
86/// assert_eq!(res.source, NamespaceSource::Default);
87/// ```
88pub fn detect_namespace(explicit: Option<&str>) -> Result<NamespaceResolution, AppError> {
89 let cwd = std::env::current_dir().map_err(AppError::Io)?;
90 let cwd_display = normalize_path(&cwd);
91
92 if let Some(ns) = explicit {
93 validate_namespace(ns)?;
94 return Ok(NamespaceResolution {
95 namespace: ns.to_owned(),
96 source: NamespaceSource::ExplicitFlag,
97 cwd: cwd_display,
98 });
99 }
100
101 if let Ok(ns) = std::env::var("SQLITE_GRAPHRAG_NAMESPACE") {
102 if !ns.is_empty() {
103 validate_namespace(&ns)?;
104 return Ok(NamespaceResolution {
105 namespace: ns,
106 source: NamespaceSource::Environment,
107 cwd: cwd_display,
108 });
109 }
110 }
111
112 Ok(NamespaceResolution {
113 namespace: "global".to_owned(),
114 source: NamespaceSource::Default,
115 cwd: cwd_display,
116 })
117}
118
119fn validate_namespace(ns: &str) -> Result<(), AppError> {
120 if ns.is_empty() || ns.len() > 80 {
121 return Err(AppError::Validation(validation::namespace_length()));
122 }
123 if !ns
124 .chars()
125 .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
126 {
127 return Err(AppError::Validation(validation::namespace_format()));
128 }
129 Ok(())
130}
131
132fn normalize_path(path: &Path) -> String {
133 path.canonicalize()
134 .unwrap_or_else(|_| path.to_path_buf())
135 .display()
136 .to_string()
137}