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#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
12#[serde(rename_all = "snake_case")]
13pub enum NamespaceSource {
14    ExplicitFlag,
15    /// Resolved from XDG `namespace.default` (`config set`).
16    #[serde(alias = "environment", alias = "Environment")]
17    XdgConfig,
18    Default,
19}
20
21#[derive(Debug, Clone, Serialize)]
22pub struct NamespaceResolution {
23    pub namespace: String,
24    pub source: NamespaceSource,
25    pub cwd: String,
26}
27
28/// Resolves the active namespace, returning only the final name.
29///
30/// Shortcut over [`detect_namespace`] when the source does not matter.
31/// With a valid explicit flag, the returned namespace is exactly the passed value.
32/// Without a flag, the final fallback is `"global"`.
33///
34/// # Errors
35///
36/// Returns [`AppError::Validation`] if `explicit` contains invalid characters
37/// or exceeds 80 characters.
38///
39/// # Examples
40///
41/// ```
42/// use sqlite_graphrag::namespace::resolve_namespace;
43///
44/// // A valid explicit flag is accepted and reflected in the result.
45/// let ns = resolve_namespace(Some("meu-projeto")).unwrap();
46/// assert_eq!(ns, "meu-projeto");
47/// ```
48///
49/// ```
50/// use sqlite_graphrag::namespace::resolve_namespace;
51/// use sqlite_graphrag::errors::AppError;
52///
53/// // Namespace with invalid characters causes a validation error (exit 1).
54/// let err = resolve_namespace(Some("ns with space")).unwrap_err();
55/// assert_eq!(err.exit_code(), 1);
56/// ```
57pub fn resolve_namespace(explicit: Option<&str>) -> Result<String, AppError> {
58    Ok(detect_namespace(explicit)?.namespace)
59}
60
61/// Resolves the active namespace, returning a struct with the source and current directory.
62///
63/// Precedence: explicit flag > XDG `namespace.default` > fallback `"global"`.
64///
65/// # Errors
66///
67/// Returns [`AppError::Validation`] if the resolved namespace contains invalid characters.
68///
69/// # Examples
70///
71/// ```
72/// use sqlite_graphrag::namespace::{detect_namespace, NamespaceSource};
73///
74/// // With an explicit flag, the source is `ExplicitFlag`.
75/// let res = detect_namespace(Some("producao")).unwrap();
76/// assert_eq!(res.namespace, "producao");
77/// assert_eq!(res.source, NamespaceSource::ExplicitFlag);
78/// ```
79///
80/// ```
81/// use sqlite_graphrag::namespace::{detect_namespace, NamespaceSource};
82///
83/// // Without any explicit configuration, fallback is "global".
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(Some(ns)) = crate::config::get_setting("namespace.default") {
102        if !ns.is_empty() {
103            validate_namespace(&ns)?;
104            return Ok(NamespaceResolution {
105                namespace: ns,
106                source: NamespaceSource::XdgConfig,
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}