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 #[inline(always)]
53 pub const fn sensitivity(self) -> Option<Sensitivity> {
54 match self {
55 Self::Sensitive { rule, .. } => Some(rule.sensitivity()),
56 Self::Allowed { .. } | Self::Unknown => None,
57 }
58 }
59
60 /// Returns the canonical configured field that matched.
61 ///
62 /// # Returns
63 ///
64 /// A field name borrowed from the policy for sensitive and allowed
65 /// classifications, or `None` for [`Self::Unknown`].
66 #[must_use]
67 #[inline(always)]
68 pub const fn matched_field(self) -> Option<&'a str> {
69 match self {
70 Self::Sensitive { rule, .. } => Some(rule.field()),
71 Self::Allowed { rule, .. } => Some(rule.field()),
72 Self::Unknown => None,
73 }
74 }
75
76 /// Returns the canonical input candidate that matched the configured rule.
77 ///
78 /// # Returns
79 ///
80 /// The exact input or a semantic token suffix for sensitive and allowed
81 /// classifications, or `None` for [`Self::Unknown`].
82 #[must_use]
83 #[inline(always)]
84 pub const fn match_kind(self) -> Option<FieldMatchKind> {
85 match self {
86 Self::Sensitive { match_kind, .. } | Self::Allowed { match_kind, .. } => Some(match_kind),
87 Self::Unknown => None,
88 }
89 }
90
91 /// Reports whether an allow rule classified the field.
92 ///
93 /// # Returns
94 ///
95 /// `true` only for [`Self::Allowed`].
96 #[inline(always)]
97 #[must_use]
98 pub const fn is_allowed(self) -> bool {
99 matches!(self, Self::Allowed { .. })
100 }
101
102 /// Reports whether no configured rule classified the field.
103 ///
104 /// # Returns
105 ///
106 /// `true` only for [`Self::Unknown`].
107 #[inline(always)]
108 #[must_use]
109 pub const fn is_unknown(self) -> bool {
110 matches!(self, Self::Unknown)
111 }
112}