1use std::fmt;
56use std::sync::Arc;
57
58use serde::{Deserialize, Serialize};
59
60use crate::filter_ir::{AuthScope, FilterIR, FilterBuilder};
61
62#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
71pub struct Namespace(String);
72
73impl Namespace {
74 pub const MAX_LENGTH: usize = 256;
76
77 pub fn new(name: impl Into<String>) -> Result<Self, NamespaceError> {
85 let name = name.into();
86 Self::validate(&name)?;
87 Ok(Self(name))
88 }
89
90 #[allow(dead_code)]
95 pub(crate) fn new_unchecked(name: impl Into<String>) -> Self {
96 Self(name.into())
97 }
98
99 fn validate(name: &str) -> Result<(), NamespaceError> {
101 if name.is_empty() {
102 return Err(NamespaceError::Empty);
103 }
104
105 if name.len() > Self::MAX_LENGTH {
106 return Err(NamespaceError::TooLong {
107 length: name.len(),
108 max: Self::MAX_LENGTH,
109 });
110 }
111
112 let first = name.chars().next().unwrap();
114 if first == '.' || first == '-' {
115 return Err(NamespaceError::InvalidStart(first));
116 }
117
118 for (i, ch) in name.chars().enumerate() {
120 if !ch.is_alphanumeric() && ch != '_' && ch != '-' && ch != '.' {
121 return Err(NamespaceError::InvalidChar { ch, position: i });
122 }
123 }
124
125 Ok(())
126 }
127
128 pub fn as_str(&self) -> &str {
130 &self.0
131 }
132
133 pub fn into_string(self) -> String {
135 self.0
136 }
137}
138
139impl fmt::Display for Namespace {
140 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141 write!(f, "{}", self.0)
142 }
143}
144
145impl AsRef<str> for Namespace {
146 fn as_ref(&self) -> &str {
147 &self.0
148 }
149}
150
151#[derive(Debug, Clone, thiserror::Error)]
153pub enum NamespaceError {
154 #[error("namespace cannot be empty")]
155 Empty,
156
157 #[error("namespace too long: {length} > {max}")]
158 TooLong { length: usize, max: usize },
159
160 #[error("namespace cannot start with '{0}'")]
161 InvalidStart(char),
162
163 #[error("invalid character '{ch}' at position {position}")]
164 InvalidChar { ch: char, position: usize },
165}
166
167#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
173pub enum NamespaceScope {
174 Single(Namespace),
176
177 Multiple(Vec<Namespace>),
179}
180
181impl NamespaceScope {
182 pub fn single(ns: Namespace) -> Self {
184 Self::Single(ns)
185 }
186
187 pub fn multiple(namespaces: Vec<Namespace>) -> Result<Self, NamespaceError> {
189 if namespaces.is_empty() {
190 return Err(NamespaceError::Empty);
191 }
192 Ok(Self::Multiple(namespaces))
193 }
194
195 pub fn namespaces(&self) -> Vec<&Namespace> {
197 match self {
198 Self::Single(ns) => vec![ns],
199 Self::Multiple(nss) => nss.iter().collect(),
200 }
201 }
202
203 pub fn contains(&self, ns: &Namespace) -> bool {
205 match self {
206 Self::Single(single) => single == ns,
207 Self::Multiple(multiple) => multiple.contains(ns),
208 }
209 }
210
211 pub fn validate_against(&self, auth: &AuthScope) -> Result<(), ScopeError> {
213 for ns in self.namespaces() {
214 if !auth.is_namespace_allowed(ns.as_str()) {
215 return Err(ScopeError::NamespaceNotAllowed(ns.clone()));
216 }
217 }
218 Ok(())
219 }
220
221 pub fn to_filter_ir(&self) -> FilterIR {
223 match self {
224 Self::Single(ns) => FilterBuilder::new()
225 .namespace(ns.as_str())
226 .build(),
227 Self::Multiple(nss) => {
228 use crate::filter_ir::{FilterAtom, FilterValue};
229 FilterIR::from_atom(FilterAtom::in_set(
230 "namespace",
231 nss.iter()
232 .map(|ns| FilterValue::String(ns.as_str().to_string()))
233 .collect(),
234 ))
235 }
236 }
237 }
238}
239
240impl fmt::Display for NamespaceScope {
241 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242 match self {
243 Self::Single(ns) => write!(f, "{}", ns),
244 Self::Multiple(nss) => {
245 let names: Vec<_> = nss.iter().map(|ns| ns.as_str()).collect();
246 write!(f, "[{}]", names.join(", "))
247 }
248 }
249 }
250}
251
252#[derive(Debug, Clone, thiserror::Error)]
254pub enum ScopeError {
255 #[error("namespace not allowed: {0}")]
256 NamespaceNotAllowed(Namespace),
257
258 #[error("auth scope expired")]
259 AuthExpired,
260
261 #[error("insufficient capabilities for this operation")]
262 InsufficientCapabilities,
263}
264
265#[derive(Debug, Clone)]
274pub struct ScopedQuery<Q> {
275 scope: NamespaceScope,
277
278 query: Q,
280
281 filters: FilterIR,
283}
284
285impl<Q> ScopedQuery<Q> {
286 pub fn new(scope: NamespaceScope, query: Q) -> Self {
290 Self {
291 scope,
292 query,
293 filters: FilterIR::all(),
294 }
295 }
296
297 pub fn in_namespace(namespace: Namespace, query: Q) -> Self {
299 Self::new(NamespaceScope::Single(namespace), query)
300 }
301
302 pub fn with_filters(mut self, filters: FilterIR) -> Self {
304 self.filters = filters;
305 self
306 }
307
308 pub fn scope(&self) -> &NamespaceScope {
310 &self.scope
311 }
312
313 pub fn query(&self) -> &Q {
315 &self.query
316 }
317
318 pub fn filters(&self) -> &FilterIR {
320 &self.filters
321 }
322
323 pub fn effective_filter(&self) -> FilterIR {
327 self.scope.to_filter_ir().and(self.filters.clone())
328 }
329
330 pub fn validate(&self, auth: &AuthScope) -> Result<(), ScopeError> {
332 if auth.is_expired() {
334 return Err(ScopeError::AuthExpired);
335 }
336
337 self.scope.validate_against(auth)?;
339
340 Ok(())
341 }
342
343 pub fn into_query(self) -> Q {
345 self.query
346 }
347}
348
349#[derive(Debug, Clone)]
361pub struct QueryRequest<Q> {
362 query: ScopedQuery<Q>,
364
365 auth: Arc<AuthScope>,
367}
368
369impl<Q> QueryRequest<Q> {
370 pub fn new(query: ScopedQuery<Q>, auth: Arc<AuthScope>) -> Result<Self, ScopeError> {
375 query.validate(&auth)?;
376 Ok(Self { query, auth })
377 }
378
379 pub fn query(&self) -> &ScopedQuery<Q> {
381 &self.query
382 }
383
384 pub fn auth(&self) -> &AuthScope {
386 &self.auth
387 }
388
389 pub fn effective_filter(&self) -> FilterIR {
396 self.auth.to_filter_ir()
397 .and(self.query.effective_filter())
398 }
399
400 pub fn namespace_scope(&self) -> &NamespaceScope {
402 self.query.scope()
403 }
404}
405
406pub fn ns(name: &str) -> Result<Namespace, NamespaceError> {
412 Namespace::new(name)
413}
414
415pub fn scope(name: &str) -> Result<NamespaceScope, NamespaceError> {
417 Ok(NamespaceScope::Single(Namespace::new(name)?))
418}
419
420#[cfg(test)]
425mod tests {
426 use super::*;
427
428 #[test]
429 fn test_namespace_validation() {
430 assert!(Namespace::new("production").is_ok());
432 assert!(Namespace::new("my_namespace").is_ok());
433 assert!(Namespace::new("project-123").is_ok());
434 assert!(Namespace::new("v1.0.0").is_ok());
435
436 assert!(Namespace::new("").is_err()); assert!(Namespace::new("-starts-with-dash").is_err());
439 assert!(Namespace::new(".starts-with-dot").is_err());
440 assert!(Namespace::new("has spaces").is_err());
441 assert!(Namespace::new("has@symbol").is_err());
442 }
443
444 #[test]
445 fn test_namespace_scope_single() {
446 let ns = Namespace::new("production").unwrap();
447 let scope = NamespaceScope::single(ns.clone());
448
449 assert!(scope.contains(&ns));
450 assert!(!scope.contains(&Namespace::new("staging").unwrap()));
451 }
452
453 #[test]
454 fn test_namespace_scope_multiple() {
455 let ns1 = Namespace::new("prod").unwrap();
456 let ns2 = Namespace::new("staging").unwrap();
457 let scope = NamespaceScope::multiple(vec![ns1.clone(), ns2.clone()]).unwrap();
458
459 assert!(scope.contains(&ns1));
460 assert!(scope.contains(&ns2));
461 assert!(!scope.contains(&Namespace::new("dev").unwrap()));
462 }
463
464 #[test]
465 fn test_scope_to_filter_ir() {
466 let scope = NamespaceScope::single(Namespace::new("production").unwrap());
467 let filter = scope.to_filter_ir();
468
469 assert!(filter.constrains_field("namespace"));
470 assert_eq!(filter.clauses.len(), 1);
471 }
472
473 #[test]
474 fn test_scoped_query_effective_filter() {
475 let ns = Namespace::new("production").unwrap();
476 let user_filter = FilterBuilder::new()
477 .eq("source", "documents")
478 .build();
479
480 let query: ScopedQuery<()> = ScopedQuery::in_namespace(ns, ())
481 .with_filters(user_filter);
482
483 let effective = query.effective_filter();
484 assert!(effective.constrains_field("namespace"));
485 assert!(effective.constrains_field("source"));
486 }
487
488 #[test]
489 fn test_query_request_validation() {
490 let ns = Namespace::new("production").unwrap();
491 let query: ScopedQuery<()> = ScopedQuery::in_namespace(ns, ());
492
493 let auth = Arc::new(AuthScope::for_namespace("production"));
495 assert!(QueryRequest::new(query.clone(), auth).is_ok());
496
497 let auth2 = Arc::new(AuthScope::for_namespace("staging"));
499 assert!(QueryRequest::new(query, auth2).is_err());
500 }
501
502 #[test]
503 fn test_query_request_effective_filter() {
504 let ns = Namespace::new("production").unwrap();
505 let query: ScopedQuery<()> = ScopedQuery::in_namespace(ns, ())
506 .with_filters(FilterBuilder::new().eq("type", "article").build());
507
508 let auth = Arc::new(
509 AuthScope::for_namespace("production")
510 .with_tenant("acme")
511 );
512
513 let request = QueryRequest::new(query, auth).unwrap();
514 let effective = request.effective_filter();
515
516 assert!(effective.constrains_field("namespace"));
518 assert!(effective.constrains_field("tenant_id"));
519 assert!(effective.constrains_field("type"));
520 }
521}