sqlite_graphrag/namespace.rs
1use crate::errors::AppError;
2use crate::i18n::validacao;
3use serde::Serialize;
4use std::path::Path;
5
6#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
7#[serde(rename_all = "snake_case")]
8pub enum NamespaceSource {
9 ExplicitFlag,
10 Environment,
11 Default,
12}
13
14#[derive(Debug, Clone, Serialize)]
15pub struct NamespaceResolution {
16 pub namespace: String,
17 pub source: NamespaceSource,
18 pub cwd: String,
19}
20
21/// Resolve o namespace ativo retornando apenas o nome final.
22///
23/// Atalho sobre [`detect_namespace`] quando a origem não importa.
24/// Com flag explícita válida, o namespace retornado é exatamente o valor passado.
25/// Sem flag, o fallback final é `"global"`.
26///
27/// # Errors
28///
29/// Retorna [`AppError::Validation`] se `explicit` contiver caracteres inválidos
30/// ou ultrapassar 80 caracteres.
31///
32/// # Examples
33///
34/// ```
35/// use sqlite_graphrag::namespace::resolve_namespace;
36///
37/// // Flag explícita válida é aceita e refletida no resultado.
38/// let ns = resolve_namespace(Some("meu-projeto")).unwrap();
39/// assert_eq!(ns, "meu-projeto");
40/// ```
41///
42/// ```
43/// use sqlite_graphrag::namespace::resolve_namespace;
44/// use sqlite_graphrag::errors::AppError;
45///
46/// // Namespace com caracteres inválidos causa erro de validação (exit 1).
47/// let err = resolve_namespace(Some("ns com espaço")).unwrap_err();
48/// assert_eq!(err.exit_code(), 1);
49/// ```
50pub fn resolve_namespace(explicit: Option<&str>) -> Result<String, AppError> {
51 Ok(detect_namespace(explicit)?.namespace)
52}
53
54/// Resolve o namespace ativo retornando estrutura com origem e diretório atual.
55///
56/// A precedência é: flag explícita > `SQLITE_GRAPHRAG_NAMESPACE` > fallback `"global"`.
57///
58/// # Errors
59///
60/// Retorna [`AppError::Validation`] se o namespace resolvido contiver caracteres inválidos.
61///
62/// # Examples
63///
64/// ```
65/// use sqlite_graphrag::namespace::{detect_namespace, NamespaceSource};
66///
67/// // Com flag explícita, a fonte é `ExplicitFlag`.
68/// let res = detect_namespace(Some("producao")).unwrap();
69/// assert_eq!(res.namespace, "producao");
70/// assert_eq!(res.source, NamespaceSource::ExplicitFlag);
71/// ```
72///
73/// ```
74/// use sqlite_graphrag::namespace::{detect_namespace, NamespaceSource};
75///
76/// // Sem nenhuma configuração explícita, fallback é "global".
77/// // Desativa env var para garantir comportamento determinístico.
78/// std::env::remove_var("SQLITE_GRAPHRAG_NAMESPACE");
79/// let res = detect_namespace(None).unwrap();
80/// assert_eq!(res.namespace, "global");
81/// assert_eq!(res.source, NamespaceSource::Default);
82/// ```
83pub fn detect_namespace(explicit: Option<&str>) -> Result<NamespaceResolution, AppError> {
84 let cwd = std::env::current_dir().map_err(AppError::Io)?;
85 let cwd_display = normalize_path(&cwd);
86
87 if let Some(ns) = explicit {
88 validate_namespace(ns)?;
89 return Ok(NamespaceResolution {
90 namespace: ns.to_owned(),
91 source: NamespaceSource::ExplicitFlag,
92 cwd: cwd_display,
93 });
94 }
95
96 if let Ok(ns) = std::env::var("SQLITE_GRAPHRAG_NAMESPACE") {
97 if !ns.is_empty() {
98 validate_namespace(&ns)?;
99 return Ok(NamespaceResolution {
100 namespace: ns,
101 source: NamespaceSource::Environment,
102 cwd: cwd_display,
103 });
104 }
105 }
106
107 Ok(NamespaceResolution {
108 namespace: "global".to_owned(),
109 source: NamespaceSource::Default,
110 cwd: cwd_display,
111 })
112}
113
114fn validate_namespace(ns: &str) -> Result<(), AppError> {
115 if ns.is_empty() || ns.len() > 80 {
116 return Err(AppError::Validation(validacao::namespace_comprimento()));
117 }
118 if !ns
119 .chars()
120 .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
121 {
122 return Err(AppError::Validation(validacao::namespace_formato()));
123 }
124 Ok(())
125}
126
127fn normalize_path(path: &Path) -> String {
128 path.canonicalize()
129 .unwrap_or_else(|_| path.to_path_buf())
130 .display()
131 .to_string()
132}