qubit_redact/policy/field/field_classification.rs
1// =============================================================================
2// Copyright (c) 2025 - 2026 Haixing Hu.
3//
4// SPDX-License-Identifier: Apache-2.0
5//
6// Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Explainable borrowed results from field-policy classification.
9
10use super::AllowRule;
11use super::FieldMatchKind;
12use super::SensitiveFieldRule;
13use super::Sensitivity;
14
15/// Explains why a field is sensitive, allowed, or unknown to a policy.
16///
17/// Matched rules borrow canonical field names from the immutable policy, so
18/// callers can inspect classification without cloning rule metadata.
19///
20/// # Type Parameters
21///
22/// * `'a` - Lifetime of canonical field names borrowed from the policy.
23#[non_exhaustive]
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum FieldClassification<'a> {
26 /// A sensitive rule classified the field.
27 Sensitive {
28 /// Borrowed configured rule that supplied the sensitivity.
29 rule: SensitiveFieldRule<'a>,
30 /// The canonical input candidate that matched this rule.
31 match_kind: FieldMatchKind,
32 },
33 /// An allow rule took precedence over sensitivity at the same candidate.
34 Allowed {
35 /// Borrowed configured rule that allowed the field.
36 rule: AllowRule<'a>,
37 /// The canonical input candidate that matched this rule.
38 match_kind: FieldMatchKind,
39 },
40 /// No configured rule classified the field.
41 Unknown,
42}
43
44impl<'a> FieldClassification<'a> {
45 /// Returns the configured sensitivity when the field is sensitive.
46 ///
47 /// # Returns
48 ///
49 /// `Some(level)` for [`Self::Sensitive`], or `None` for allowed and unknown
50 /// fields.
51 #[must_use]
52 pub const fn sensitivity(self) -> Option<Sensitivity> {
53 match self {
54 Self::Sensitive { rule, .. } => Some(rule.sensitivity()),
55 Self::Allowed { .. } | Self::Unknown => None,
56 }
57 }
58
59 /// Returns the canonical configured field that matched.
60 ///
61 /// # Returns
62 ///
63 /// A field name borrowed from the policy for sensitive and allowed
64 /// classifications, or `None` for [`Self::Unknown`].
65 #[must_use]
66 pub const fn matched_field(self) -> Option<&'a str> {
67 match self {
68 Self::Sensitive { rule, .. } => Some(rule.field()),
69 Self::Allowed { rule, .. } => Some(rule.field()),
70 Self::Unknown => None,
71 }
72 }
73
74 /// Returns the canonical input candidate that matched the configured rule.
75 ///
76 /// # Returns
77 ///
78 /// The exact input or a semantic token suffix for sensitive and allowed
79 /// classifications, or `None` for [`Self::Unknown`].
80 #[must_use]
81 pub const fn match_kind(self) -> Option<FieldMatchKind> {
82 match self {
83 Self::Sensitive { match_kind, .. } | Self::Allowed { match_kind, .. } => Some(match_kind),
84 Self::Unknown => None,
85 }
86 }
87
88 /// Reports whether an allow rule classified the field.
89 ///
90 /// # Returns
91 ///
92 /// `true` only for [`Self::Allowed`].
93 #[inline(always)]
94 #[must_use]
95 pub const fn is_allowed(self) -> bool {
96 matches!(self, Self::Allowed { .. })
97 }
98
99 /// Reports whether no configured rule classified the field.
100 ///
101 /// # Returns
102 ///
103 /// `true` only for [`Self::Unknown`].
104 #[inline(always)]
105 #[must_use]
106 pub const fn is_unknown(self) -> bool {
107 matches!(self, Self::Unknown)
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::FieldClassification;
114
115 #[test]
116 fn unknown_predicates_are_mutually_exclusive() {
117 let classification = FieldClassification::Unknown;
118
119 assert!(!classification.is_allowed());
120 assert!(classification.is_unknown());
121 }
122}