Skip to main content

steel_core/permission/
metadata.rs

1use std::{error::Error, fmt, mem};
2
3use serde::{Deserialize, Serialize};
4use steel_utils::Identifier;
5
6use super::{
7    PermissionContext, PermissionContextKeyError, PermissionResolutionSource,
8    PermissionRuleContext, PermissionRuleContextError,
9    rule_expression::{PermissionExpressionContextError, parse_context, write_context},
10};
11
12/// A typed value attached to a permission subject or group.
13#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
14#[serde(untagged)]
15pub enum PermissionMetadataValue {
16    /// Boolean metadata.
17    Bool(bool),
18    /// Signed integer metadata.
19    Integer(i64),
20    /// Text metadata.
21    String(String),
22}
23
24impl PermissionMetadataValue {
25    /// Returns the boolean value when the stored type matches.
26    #[must_use]
27    pub const fn as_bool(&self) -> Option<bool> {
28        match self {
29            Self::Bool(value) => Some(*value),
30            Self::Integer(_) | Self::String(_) => None,
31        }
32    }
33
34    /// Returns the integer value when the stored type matches.
35    #[must_use]
36    pub const fn as_i64(&self) -> Option<i64> {
37        match self {
38            Self::Integer(value) => Some(*value),
39            Self::Bool(_) | Self::String(_) => None,
40        }
41    }
42
43    /// Returns the text value when the stored type matches.
44    #[must_use]
45    pub fn as_str(&self) -> Option<&str> {
46        match self {
47            Self::String(value) => Some(value),
48            Self::Bool(_) | Self::Integer(_) => None,
49        }
50    }
51}
52
53impl fmt::Display for PermissionMetadataValue {
54    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
55        match self {
56            Self::Bool(value) => value.fmt(formatter),
57            Self::Integer(value) => value.fmt(formatter),
58            Self::String(value) => formatter.write_str(value),
59        }
60    }
61}
62
63/// A namespaced metadata key and its optional rule-side context selector.
64#[derive(Clone, Debug, PartialEq, Eq)]
65pub struct PermissionMetadataExpression {
66    key: Identifier,
67    context: PermissionRuleContext,
68}
69
70impl PermissionMetadataExpression {
71    /// Creates an expression from validated parts.
72    #[must_use]
73    pub const fn new(key: Identifier, context: PermissionRuleContext) -> Self {
74        Self { key, context }
75    }
76
77    /// Parses `namespace:path` or `namespace:path{context=value,...}`.
78    ///
79    /// # Errors
80    ///
81    /// Returns an error when the metadata key or context selector is invalid.
82    pub fn parse(value: impl Into<String>) -> Result<Self, PermissionMetadataExpressionError> {
83        let value = value.into();
84        let Some(context_start) = value.find('{') else {
85            let key = parse_permission_metadata_key(value.as_str()).map_err(|source| {
86                PermissionMetadataExpressionError::InvalidMetadataKey { value, source }
87            })?;
88            return Ok(Self::new(key, PermissionRuleContext::Global));
89        };
90
91        if !value.ends_with('}') {
92            return Err(PermissionMetadataExpressionError::UnclosedContext);
93        }
94
95        let key_value = &value[..context_start];
96        let key = parse_permission_metadata_key(key_value).map_err(|source| {
97            PermissionMetadataExpressionError::InvalidMetadataKey {
98                value: key_value.to_owned(),
99                source,
100            }
101        })?;
102        let context_value = &value[context_start + 1..value.len() - 1];
103        let context = parse_context(context_value)
104            .map_err(PermissionMetadataExpressionError::from_context_error)?;
105        Ok(Self::new(key, context))
106    }
107
108    /// Returns the metadata key.
109    #[must_use]
110    pub const fn key(&self) -> &Identifier {
111        &self.key
112    }
113
114    /// Returns the rule-side context selector.
115    #[must_use]
116    pub const fn context(&self) -> &PermissionRuleContext {
117        &self.context
118    }
119
120    /// Splits the expression into its key and context.
121    #[must_use]
122    pub fn into_parts(self) -> (Identifier, PermissionRuleContext) {
123        (self.key, self.context)
124    }
125}
126
127impl fmt::Display for PermissionMetadataExpression {
128    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
129        self.key.fmt(formatter)?;
130        write_context(formatter, &self.context)
131    }
132}
133
134/// Parses a namespaced permission metadata key.
135///
136/// # Errors
137///
138/// Returns an error unless the key is a non-empty `namespace:path` identifier.
139pub fn parse_permission_metadata_key(
140    value: impl Into<String>,
141) -> Result<Identifier, PermissionMetadataKeyError> {
142    let value = value.into();
143    let Some((namespace, path)) = value.split_once(':') else {
144        return Err(PermissionMetadataKeyError::InvalidFormat);
145    };
146    if namespace.is_empty() {
147        return Err(PermissionMetadataKeyError::EmptyNamespace);
148    }
149    if path.is_empty() {
150        return Err(PermissionMetadataKeyError::EmptyPath);
151    }
152    if path.contains(':') {
153        return Err(PermissionMetadataKeyError::InvalidFormat);
154    }
155    if namespace.split('.').any(str::is_empty) {
156        return Err(PermissionMetadataKeyError::InvalidNamespace);
157    }
158    if path.split(['.', '/']).any(str::is_empty) {
159        return Err(PermissionMetadataKeyError::InvalidPath);
160    }
161    if !Identifier::validate_namespace(namespace) {
162        return Err(PermissionMetadataKeyError::InvalidNamespace);
163    }
164    if !Identifier::validate_path(path) {
165        return Err(PermissionMetadataKeyError::InvalidPath);
166    }
167    Ok(Identifier::new(namespace.to_owned(), path.to_owned()))
168}
169
170/// Invalid permission metadata identifier.
171#[derive(Clone, Copy, Debug, PartialEq, Eq)]
172pub enum PermissionMetadataKeyError {
173    /// The value does not use exactly one `namespace:path` separator.
174    InvalidFormat,
175    /// The namespace is empty.
176    EmptyNamespace,
177    /// The path is empty.
178    EmptyPath,
179    /// The namespace contains invalid characters or empty segments.
180    InvalidNamespace,
181    /// The path contains invalid characters or empty segments.
182    InvalidPath,
183}
184
185impl fmt::Display for PermissionMetadataKeyError {
186    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
187        match self {
188            Self::InvalidFormat => {
189                formatter.write_str("permission metadata key must be a namespaced id")
190            }
191            Self::EmptyNamespace => {
192                formatter.write_str("permission metadata key namespace is empty")
193            }
194            Self::EmptyPath => formatter.write_str("permission metadata key path is empty"),
195            Self::InvalidNamespace => {
196                formatter.write_str("permission metadata key namespace contains invalid characters")
197            }
198            Self::InvalidPath => {
199                formatter.write_str("permission metadata key path contains invalid characters")
200            }
201        }
202    }
203}
204
205impl Error for PermissionMetadataKeyError {}
206
207/// Invalid permission metadata expression syntax.
208#[derive(Clone, Debug, PartialEq, Eq)]
209pub enum PermissionMetadataExpressionError {
210    /// The metadata key is invalid.
211    InvalidMetadataKey {
212        /// Invalid key text.
213        value: String,
214        /// Key parse error.
215        source: PermissionMetadataKeyError,
216    },
217    /// A context selector starts with `{` but does not end with `}`.
218    UnclosedContext,
219    /// The context selector contains no entries.
220    EmptyContext,
221    /// A context entry is not `key=value`.
222    InvalidContextEntry(String),
223    /// A context entry contains an unsupported value.
224    InvalidContextValue {
225        /// Context key.
226        key: String,
227        /// Invalid context value.
228        value: String,
229    },
230    /// The same context key appears more than once.
231    DuplicateContextKey(String),
232    /// The built-in domain value is invalid.
233    InvalidDomain(String),
234    /// The built-in loaded-world value is invalid.
235    InvalidWorld(String),
236    /// A custom context key is invalid.
237    InvalidContextKey {
238        /// Invalid context key text.
239        key: String,
240        /// Context key parse error.
241        source: PermissionContextKeyError,
242    },
243    /// The combined rule-side context is invalid.
244    InvalidRuleContext(PermissionRuleContextError),
245}
246
247impl PermissionMetadataExpressionError {
248    fn from_context_error(error: PermissionExpressionContextError) -> Self {
249        match error {
250            PermissionExpressionContextError::EmptyContext => Self::EmptyContext,
251            PermissionExpressionContextError::InvalidContextEntry(entry) => {
252                Self::InvalidContextEntry(entry)
253            }
254            PermissionExpressionContextError::InvalidContextValue { key, value } => {
255                Self::InvalidContextValue { key, value }
256            }
257            PermissionExpressionContextError::DuplicateContextKey(key) => {
258                Self::DuplicateContextKey(key)
259            }
260            PermissionExpressionContextError::InvalidDomain(domain) => Self::InvalidDomain(domain),
261            PermissionExpressionContextError::InvalidWorld(world) => Self::InvalidWorld(world),
262            PermissionExpressionContextError::InvalidContextKey { key, source } => {
263                Self::InvalidContextKey { key, source }
264            }
265            PermissionExpressionContextError::InvalidRuleContext(source) => {
266                Self::InvalidRuleContext(source)
267            }
268        }
269    }
270}
271
272impl fmt::Display for PermissionMetadataExpressionError {
273    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
274        match self {
275            Self::InvalidMetadataKey { value, source } => {
276                write!(
277                    formatter,
278                    "invalid permission metadata key '{value}': {source}"
279                )
280            }
281            Self::UnclosedContext => {
282                formatter.write_str("permission metadata context selector is not closed")
283            }
284            Self::EmptyContext => {
285                formatter.write_str("permission metadata context selector is empty")
286            }
287            Self::InvalidContextEntry(entry) => {
288                write!(
289                    formatter,
290                    "invalid permission metadata context entry '{entry}'"
291                )
292            }
293            Self::InvalidContextValue { key, value } => write!(
294                formatter,
295                "invalid permission metadata context value '{value}' for '{key}'"
296            ),
297            Self::DuplicateContextKey(key) => write!(
298                formatter,
299                "permission metadata context key '{key}' appears more than once"
300            ),
301            Self::InvalidDomain(domain) => write!(formatter, "invalid domain context '{domain}'"),
302            Self::InvalidWorld(world) => write!(formatter, "invalid world context '{world}'"),
303            Self::InvalidContextKey { key, source } => {
304                write!(
305                    formatter,
306                    "invalid permission context key '{key}': {source}"
307                )
308            }
309            Self::InvalidRuleContext(source) => source.fmt(formatter),
310        }
311    }
312}
313
314impl Error for PermissionMetadataExpressionError {}
315
316/// One permission metadata rule.
317#[derive(Clone, Debug, PartialEq, Eq)]
318pub struct PermissionMetadataEntry {
319    key: Identifier,
320    context: PermissionRuleContext,
321    value: PermissionMetadataValue,
322}
323
324impl PermissionMetadataEntry {
325    /// Creates a global metadata rule.
326    #[must_use]
327    pub const fn new(key: Identifier, value: PermissionMetadataValue) -> Self {
328        Self {
329            key,
330            context: PermissionRuleContext::Global,
331            value,
332        }
333    }
334
335    /// Creates a contextual metadata rule.
336    #[must_use]
337    pub const fn new_with_context(
338        key: Identifier,
339        context: PermissionRuleContext,
340        value: PermissionMetadataValue,
341    ) -> Self {
342        Self {
343            key,
344            context,
345            value,
346        }
347    }
348
349    /// Returns the metadata key.
350    #[must_use]
351    pub const fn key(&self) -> &Identifier {
352        &self.key
353    }
354
355    /// Returns the rule-side context selector.
356    #[must_use]
357    pub const fn context(&self) -> &PermissionRuleContext {
358        &self.context
359    }
360
361    /// Returns the configured value.
362    #[must_use]
363    pub const fn value(&self) -> &PermissionMetadataValue {
364        &self.value
365    }
366}
367
368/// A flat effective permission metadata set.
369#[derive(Clone, Debug, Default, PartialEq, Eq)]
370pub struct PermissionMetadataSet {
371    entries: Vec<PermissionMetadataEntry>,
372    sources: Vec<PermissionResolutionSource>,
373}
374
375impl PermissionMetadataSet {
376    /// Creates an empty metadata set.
377    #[must_use]
378    pub const fn new() -> Self {
379        Self {
380            entries: Vec::new(),
381            sources: Vec::new(),
382        }
383    }
384
385    /// Creates a subject metadata set from entries.
386    #[must_use]
387    pub fn from_entries(entries: impl IntoIterator<Item = PermissionMetadataEntry>) -> Self {
388        let entries = entries.into_iter().collect::<Vec<_>>();
389        let sources = vec![PermissionResolutionSource::Subject; entries.len()];
390        Self { entries, sources }
391    }
392
393    /// Returns all entries in insertion order.
394    #[must_use]
395    pub fn entries(&self) -> &[PermissionMetadataEntry] {
396        &self.entries
397    }
398
399    /// Returns whether the set contains no entries.
400    #[must_use]
401    pub const fn is_empty(&self) -> bool {
402        self.entries.is_empty()
403    }
404
405    /// Adds one subject metadata rule.
406    pub fn push(&mut self, entry: PermissionMetadataEntry) {
407        self.push_with_source(entry, PermissionResolutionSource::Subject);
408    }
409
410    /// Sets one exact global value, replacing any previous exact value.
411    pub fn set(&mut self, key: Identifier, value: PermissionMetadataValue) {
412        self.set_in(key, PermissionRuleContext::Global, value);
413    }
414
415    /// Sets one exact contextual value, replacing any previous exact value.
416    pub fn set_in(
417        &mut self,
418        key: Identifier,
419        context: PermissionRuleContext,
420        value: PermissionMetadataValue,
421    ) {
422        self.retain_entries(|entry| entry.key != key || entry.context != context);
423        self.push(PermissionMetadataEntry::new_with_context(
424            key, context, value,
425        ));
426    }
427
428    /// Removes one exact global value.
429    pub fn unset(&mut self, key: &Identifier) -> bool {
430        self.unset_in(key, &PermissionRuleContext::Global)
431    }
432
433    /// Removes one exact contextual value.
434    pub fn unset_in(&mut self, key: &Identifier, context: &PermissionRuleContext) -> bool {
435        let old_len = self.entries.len();
436        self.retain_entries(|entry| entry.key() != key || entry.context() != context);
437        self.entries.len() != old_len
438    }
439
440    /// Resolves one value in the global context.
441    #[must_use]
442    pub fn resolve(&self, key: &Identifier) -> Option<&PermissionMetadataValue> {
443        self.resolve_in(key, &PermissionContext::global())
444    }
445
446    /// Resolves one value in an active permission context.
447    ///
448    /// More-specific contexts win. Ties prefer subject metadata, then group
449    /// priority, then the final insertion order for deterministic resolution.
450    #[must_use]
451    pub fn resolve_in(
452        &self,
453        key: &Identifier,
454        context: &PermissionContext,
455    ) -> Option<&PermissionMetadataValue> {
456        self.best_candidate(key, context)
457            .map(|candidate| self.entries[candidate.entry_index].value())
458    }
459
460    /// Resolves one global value and returns the winning rule.
461    #[must_use]
462    pub fn resolve_detailed(&self, key: &Identifier) -> Option<PermissionMetadataResolution> {
463        self.resolve_in_detailed(key, &PermissionContext::global())
464    }
465
466    /// Resolves one contextual value and returns the winning rule.
467    #[must_use]
468    pub fn resolve_in_detailed(
469        &self,
470        key: &Identifier,
471        context: &PermissionContext,
472    ) -> Option<PermissionMetadataResolution> {
473        self.best_candidate(key, context)
474            .map(|candidate| self.resolution(candidate))
475    }
476
477    pub(super) fn push_group(
478        &mut self,
479        entry: PermissionMetadataEntry,
480        group: &str,
481        group_priority: i32,
482    ) {
483        self.push_with_source(
484            entry,
485            PermissionResolutionSource::Group {
486                name: group.to_owned(),
487                priority: group_priority,
488            },
489        );
490    }
491
492    fn push_with_source(
493        &mut self,
494        entry: PermissionMetadataEntry,
495        source: PermissionResolutionSource,
496    ) {
497        self.entries.push(entry);
498        self.sources.push(source);
499    }
500
501    fn retain_entries(&mut self, mut keep: impl FnMut(&PermissionMetadataEntry) -> bool) {
502        let entries = mem::take(&mut self.entries);
503        let sources = mem::take(&mut self.sources);
504        for (entry, source) in entries.into_iter().zip(sources) {
505            if keep(&entry) {
506                self.entries.push(entry);
507                self.sources.push(source);
508            }
509        }
510    }
511
512    fn best_candidate(
513        &self,
514        key: &Identifier,
515        context: &PermissionContext,
516    ) -> Option<PermissionMetadataCandidate> {
517        let mut best: Option<PermissionMetadataCandidate> = None;
518        for (index, (entry, source)) in self.entries.iter().zip(&self.sources).enumerate() {
519            if entry.key() != key || !entry.context().matches(context) {
520                continue;
521            }
522            let candidate = PermissionMetadataCandidate {
523                entry_index: index,
524                context_specificity: entry.context().specificity(),
525                source: source.clone(),
526            };
527            if best
528                .as_ref()
529                .is_none_or(|current| candidate.order() > current.order())
530            {
531                best = Some(candidate);
532            }
533        }
534        best
535    }
536
537    fn resolution(&self, candidate: PermissionMetadataCandidate) -> PermissionMetadataResolution {
538        PermissionMetadataResolution {
539            entry: self.entries[candidate.entry_index].clone(),
540            source: candidate.source,
541            context_specificity: candidate.context_specificity,
542            insertion_index: candidate.entry_index,
543        }
544    }
545}
546
547/// Detailed winning permission metadata rule.
548#[derive(Clone, Debug, PartialEq, Eq)]
549pub struct PermissionMetadataResolution {
550    entry: PermissionMetadataEntry,
551    source: PermissionResolutionSource,
552    context_specificity: usize,
553    insertion_index: usize,
554}
555
556impl PermissionMetadataResolution {
557    /// Returns the complete winning rule.
558    #[must_use]
559    pub const fn entry(&self) -> &PermissionMetadataEntry {
560        &self.entry
561    }
562
563    /// Returns where the winning rule came from.
564    #[must_use]
565    pub const fn source(&self) -> &PermissionResolutionSource {
566        &self.source
567    }
568
569    /// Returns the configured value.
570    #[must_use]
571    pub const fn value(&self) -> &PermissionMetadataValue {
572        self.entry.value()
573    }
574
575    /// Returns the winning metadata key.
576    #[must_use]
577    pub const fn key(&self) -> &Identifier {
578        self.entry.key()
579    }
580
581    /// Returns the winning context constraint.
582    #[must_use]
583    pub const fn context(&self) -> &PermissionRuleContext {
584        self.entry.context()
585    }
586
587    /// Returns the context-specificity rank used during resolution.
588    #[must_use]
589    pub const fn context_specificity(&self) -> usize {
590        self.context_specificity
591    }
592
593    /// Returns the insertion index used as the final deterministic tie-breaker.
594    #[must_use]
595    pub const fn insertion_index(&self) -> usize {
596        self.insertion_index
597    }
598}
599
600#[derive(Clone, Debug, PartialEq, Eq)]
601struct PermissionMetadataCandidate {
602    entry_index: usize,
603    context_specificity: usize,
604    source: PermissionResolutionSource,
605}
606
607impl PermissionMetadataCandidate {
608    const fn order(&self) -> (usize, usize, i32, usize) {
609        (
610            self.context_specificity,
611            self.source.rank(),
612            self.source.tie_priority(),
613            self.entry_index,
614        )
615    }
616}