1use std::fmt;
59use std::sync::Arc;
60
61use serde::{Deserialize, Serialize};
62
63use crate::filter_ir::{AuthScope, FilterIR, FilterBuilder};
64
65#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
74pub struct Namespace(String);
75
76impl Namespace {
77 pub const MAX_LENGTH: usize = 256;
79
80 pub fn new(name: impl Into<String>) -> Result<Self, NamespaceError> {
88 let name = name.into();
89 Self::validate(&name)?;
90 Ok(Self(name))
91 }
92
93 #[allow(dead_code)]
98 pub(crate) fn new_unchecked(name: impl Into<String>) -> Self {
99 Self(name.into())
100 }
101
102 fn validate(name: &str) -> Result<(), NamespaceError> {
104 if name.is_empty() {
105 return Err(NamespaceError::Empty);
106 }
107
108 if name.len() > Self::MAX_LENGTH {
109 return Err(NamespaceError::TooLong {
110 length: name.len(),
111 max: Self::MAX_LENGTH,
112 });
113 }
114
115 let first = name.chars().next().unwrap();
117 if first == '.' || first == '-' {
118 return Err(NamespaceError::InvalidStart(first));
119 }
120
121 for (i, ch) in name.chars().enumerate() {
123 if !ch.is_alphanumeric() && ch != '_' && ch != '-' && ch != '.' {
124 return Err(NamespaceError::InvalidChar { ch, position: i });
125 }
126 }
127
128 Ok(())
129 }
130
131 pub fn as_str(&self) -> &str {
133 &self.0
134 }
135
136 pub fn into_string(self) -> String {
138 self.0
139 }
140}
141
142impl fmt::Display for Namespace {
143 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144 write!(f, "{}", self.0)
145 }
146}
147
148impl AsRef<str> for Namespace {
149 fn as_ref(&self) -> &str {
150 &self.0
151 }
152}
153
154#[derive(Debug, Clone, thiserror::Error)]
156pub enum NamespaceError {
157 #[error("namespace cannot be empty")]
158 Empty,
159
160 #[error("namespace too long: {length} > {max}")]
161 TooLong { length: usize, max: usize },
162
163 #[error("namespace cannot start with '{0}'")]
164 InvalidStart(char),
165
166 #[error("invalid character '{ch}' at position {position}")]
167 InvalidChar { ch: char, position: usize },
168}
169
170#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
176pub enum NamespaceScope {
177 Single(Namespace),
179
180 Multiple(Vec<Namespace>),
182}
183
184impl NamespaceScope {
185 pub fn single(ns: Namespace) -> Self {
187 Self::Single(ns)
188 }
189
190 pub fn multiple(namespaces: Vec<Namespace>) -> Result<Self, NamespaceError> {
192 if namespaces.is_empty() {
193 return Err(NamespaceError::Empty);
194 }
195 Ok(Self::Multiple(namespaces))
196 }
197
198 pub fn namespaces(&self) -> Vec<&Namespace> {
200 match self {
201 Self::Single(ns) => vec![ns],
202 Self::Multiple(nss) => nss.iter().collect(),
203 }
204 }
205
206 pub fn contains(&self, ns: &Namespace) -> bool {
208 match self {
209 Self::Single(single) => single == ns,
210 Self::Multiple(multiple) => multiple.contains(ns),
211 }
212 }
213
214 pub fn validate_against(&self, auth: &AuthScope) -> Result<(), ScopeError> {
216 for ns in self.namespaces() {
217 if !auth.is_namespace_allowed(ns.as_str()) {
218 return Err(ScopeError::NamespaceNotAllowed(ns.clone()));
219 }
220 }
221 Ok(())
222 }
223
224 pub fn to_filter_ir(&self) -> FilterIR {
226 match self {
227 Self::Single(ns) => FilterBuilder::new()
228 .namespace(ns.as_str())
229 .build(),
230 Self::Multiple(nss) => {
231 use crate::filter_ir::{FilterAtom, FilterValue};
232 FilterIR::from_atom(FilterAtom::in_set(
233 "namespace",
234 nss.iter()
235 .map(|ns| FilterValue::String(ns.as_str().to_string()))
236 .collect(),
237 ))
238 }
239 }
240 }
241}
242
243impl fmt::Display for NamespaceScope {
244 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245 match self {
246 Self::Single(ns) => write!(f, "{}", ns),
247 Self::Multiple(nss) => {
248 let names: Vec<_> = nss.iter().map(|ns| ns.as_str()).collect();
249 write!(f, "[{}]", names.join(", "))
250 }
251 }
252 }
253}
254
255#[derive(Debug, Clone, thiserror::Error)]
257pub enum ScopeError {
258 #[error("namespace not allowed: {0}")]
259 NamespaceNotAllowed(Namespace),
260
261 #[error("auth scope expired")]
262 AuthExpired,
263
264 #[error("insufficient capabilities for this operation")]
265 InsufficientCapabilities,
266}
267
268#[derive(Debug, Clone)]
277pub struct ScopedQuery<Q> {
278 scope: NamespaceScope,
280
281 query: Q,
283
284 filters: FilterIR,
286}
287
288impl<Q> ScopedQuery<Q> {
289 pub fn new(scope: NamespaceScope, query: Q) -> Self {
293 Self {
294 scope,
295 query,
296 filters: FilterIR::all(),
297 }
298 }
299
300 pub fn in_namespace(namespace: Namespace, query: Q) -> Self {
302 Self::new(NamespaceScope::Single(namespace), query)
303 }
304
305 pub fn with_filters(mut self, filters: FilterIR) -> Self {
307 self.filters = filters;
308 self
309 }
310
311 pub fn scope(&self) -> &NamespaceScope {
313 &self.scope
314 }
315
316 pub fn query(&self) -> &Q {
318 &self.query
319 }
320
321 pub fn filters(&self) -> &FilterIR {
323 &self.filters
324 }
325
326 pub fn effective_filter(&self) -> FilterIR {
330 self.scope.to_filter_ir().and(self.filters.clone())
331 }
332
333 pub fn validate(&self, auth: &AuthScope) -> Result<(), ScopeError> {
335 if auth.is_expired() {
337 return Err(ScopeError::AuthExpired);
338 }
339
340 self.scope.validate_against(auth)?;
342
343 Ok(())
344 }
345
346 pub fn into_query(self) -> Q {
348 self.query
349 }
350}
351
352#[derive(Debug, Clone)]
364pub struct QueryRequest<Q> {
365 query: ScopedQuery<Q>,
367
368 auth: Arc<AuthScope>,
370}
371
372impl<Q> QueryRequest<Q> {
373 pub fn new(query: ScopedQuery<Q>, auth: Arc<AuthScope>) -> Result<Self, ScopeError> {
378 query.validate(&auth)?;
379 Ok(Self { query, auth })
380 }
381
382 pub fn query(&self) -> &ScopedQuery<Q> {
384 &self.query
385 }
386
387 pub fn auth(&self) -> &AuthScope {
389 &self.auth
390 }
391
392 pub fn effective_filter(&self) -> FilterIR {
399 self.auth.to_filter_ir()
400 .and(self.query.effective_filter())
401 }
402
403 pub fn namespace_scope(&self) -> &NamespaceScope {
405 self.query.scope()
406 }
407}
408
409pub fn ns(name: &str) -> Result<Namespace, NamespaceError> {
415 Namespace::new(name)
416}
417
418pub fn scope(name: &str) -> Result<NamespaceScope, NamespaceError> {
420 Ok(NamespaceScope::Single(Namespace::new(name)?))
421}
422
423#[cfg(test)]
428mod tests {
429 use super::*;
430
431 #[test]
432 fn test_namespace_validation() {
433 assert!(Namespace::new("production").is_ok());
435 assert!(Namespace::new("my_namespace").is_ok());
436 assert!(Namespace::new("project-123").is_ok());
437 assert!(Namespace::new("v1.0.0").is_ok());
438
439 assert!(Namespace::new("").is_err()); assert!(Namespace::new("-starts-with-dash").is_err());
442 assert!(Namespace::new(".starts-with-dot").is_err());
443 assert!(Namespace::new("has spaces").is_err());
444 assert!(Namespace::new("has@symbol").is_err());
445 }
446
447 #[test]
448 fn test_namespace_scope_single() {
449 let ns = Namespace::new("production").unwrap();
450 let scope = NamespaceScope::single(ns.clone());
451
452 assert!(scope.contains(&ns));
453 assert!(!scope.contains(&Namespace::new("staging").unwrap()));
454 }
455
456 #[test]
457 fn test_namespace_scope_multiple() {
458 let ns1 = Namespace::new("prod").unwrap();
459 let ns2 = Namespace::new("staging").unwrap();
460 let scope = NamespaceScope::multiple(vec![ns1.clone(), ns2.clone()]).unwrap();
461
462 assert!(scope.contains(&ns1));
463 assert!(scope.contains(&ns2));
464 assert!(!scope.contains(&Namespace::new("dev").unwrap()));
465 }
466
467 #[test]
468 fn test_scope_to_filter_ir() {
469 let scope = NamespaceScope::single(Namespace::new("production").unwrap());
470 let filter = scope.to_filter_ir();
471
472 assert!(filter.constrains_field("namespace"));
473 assert_eq!(filter.clauses.len(), 1);
474 }
475
476 #[test]
477 fn test_scoped_query_effective_filter() {
478 let ns = Namespace::new("production").unwrap();
479 let user_filter = FilterBuilder::new()
480 .eq("source", "documents")
481 .build();
482
483 let query: ScopedQuery<()> = ScopedQuery::in_namespace(ns, ())
484 .with_filters(user_filter);
485
486 let effective = query.effective_filter();
487 assert!(effective.constrains_field("namespace"));
488 assert!(effective.constrains_field("source"));
489 }
490
491 #[test]
492 fn test_query_request_validation() {
493 let ns = Namespace::new("production").unwrap();
494 let query: ScopedQuery<()> = ScopedQuery::in_namespace(ns, ());
495
496 let auth = Arc::new(AuthScope::for_namespace("production"));
498 assert!(QueryRequest::new(query.clone(), auth).is_ok());
499
500 let auth2 = Arc::new(AuthScope::for_namespace("staging"));
502 assert!(QueryRequest::new(query, auth2).is_err());
503 }
504
505 #[test]
506 fn test_query_request_effective_filter() {
507 let ns = Namespace::new("production").unwrap();
508 let query: ScopedQuery<()> = ScopedQuery::in_namespace(ns, ())
509 .with_filters(FilterBuilder::new().eq("type", "article").build());
510
511 let auth = Arc::new(
512 AuthScope::for_namespace("production")
513 .with_tenant("acme")
514 );
515
516 let request = QueryRequest::new(query, auth).unwrap();
517 let effective = request.effective_filter();
518
519 assert!(effective.constrains_field("namespace"));
521 assert!(effective.constrains_field("tenant_id"));
522 assert!(effective.constrains_field("type"));
523 }
524}