Skip to main content

sqlite_graphrag/
namespace.rs

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