Skip to main content

entity_core/
policy.rs

1// SPDX-FileCopyrightText: 2025-2026 RAprogramm <andrey.rozanov.vl@gmail.com>
2// SPDX-License-Identifier: MIT
3
4//! Policy/authorization types for entity-derive.
5
6use std::fmt;
7
8/// Error type for policy-checked repository operations.
9#[derive(Debug)]
10pub enum PolicyError<R, P> {
11    /// Authorization was denied by the policy.
12    Policy(P),
13    /// Repository operation failed.
14    Repository(R)
15}
16
17impl<R, P> PolicyError<R, P> {
18    /// Check if this is a policy (authorization) error.
19    pub const fn is_policy(&self) -> bool {
20        matches!(self, Self::Policy(_))
21    }
22
23    /// Check if this is a repository (database) error.
24    pub const fn is_repository(&self) -> bool {
25        matches!(self, Self::Repository(_))
26    }
27}
28
29impl<R: fmt::Display, P: fmt::Display> fmt::Display for PolicyError<R, P> {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        match self {
32            Self::Policy(e) => write!(f, "authorization denied: {e}"),
33            Self::Repository(e) => write!(f, "repository error: {e}")
34        }
35    }
36}
37
38impl<R: std::error::Error + 'static, P: std::error::Error + 'static> std::error::Error
39    for PolicyError<R, P>
40{
41    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
42        match self {
43            Self::Policy(e) => Some(e),
44            Self::Repository(e) => Some(e)
45        }
46    }
47}
48
49/// Operation kind for policy checks.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
51pub enum PolicyOperation {
52    /// Create a new entity.
53    Create,
54    /// Read/find an entity.
55    Read,
56    /// Update an existing entity.
57    Update,
58    /// Delete an entity.
59    Delete,
60    /// List entities.
61    List,
62    /// Execute a command.
63    Command
64}
65
66impl PolicyOperation {
67    /// Check if this is a read-only operation.
68    #[must_use]
69    pub const fn is_read_only(&self) -> bool {
70        matches!(self, Self::Read | Self::List)
71    }
72
73    /// Check if this is a mutation operation.
74    #[must_use]
75    pub const fn is_mutation(&self) -> bool {
76        !self.is_read_only()
77    }
78}
79
80#[cfg(test)]
81#[allow(clippy::uninlined_format_args)]
82mod tests {
83    use std::error::Error;
84
85    use super::*;
86
87    #[derive(Debug)]
88    struct TestError(&'static str);
89
90    impl fmt::Display for TestError {
91        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92            write!(f, "{}", self.0)
93        }
94    }
95
96    impl std::error::Error for TestError {}
97
98    #[test]
99    fn policy_error_is_policy() {
100        let err: PolicyError<TestError, TestError> = PolicyError::Policy(TestError("denied"));
101        assert!(err.is_policy());
102        assert!(!err.is_repository());
103    }
104
105    #[test]
106    fn policy_error_is_repository() {
107        let err: PolicyError<TestError, TestError> = PolicyError::Repository(TestError("db"));
108        assert!(err.is_repository());
109        assert!(!err.is_policy());
110    }
111
112    #[test]
113    fn policy_error_display() {
114        let policy: PolicyError<TestError, TestError> = PolicyError::Policy(TestError("denied"));
115        assert_eq!(format!("{}", policy), "authorization denied: denied");
116
117        let repo: PolicyError<TestError, TestError> = PolicyError::Repository(TestError("db"));
118        assert_eq!(format!("{}", repo), "repository error: db");
119    }
120
121    #[test]
122    fn policy_error_source() {
123        let policy: PolicyError<TestError, TestError> = PolicyError::Policy(TestError("denied"));
124        assert!(policy.source().is_some());
125
126        let repo: PolicyError<TestError, TestError> = PolicyError::Repository(TestError("db"));
127        assert!(repo.source().is_some());
128    }
129
130    #[test]
131    fn policy_operation_is_read_only() {
132        assert!(PolicyOperation::Read.is_read_only());
133        assert!(PolicyOperation::List.is_read_only());
134        assert!(!PolicyOperation::Create.is_read_only());
135        assert!(!PolicyOperation::Update.is_read_only());
136        assert!(!PolicyOperation::Delete.is_read_only());
137        assert!(!PolicyOperation::Command.is_read_only());
138    }
139
140    #[test]
141    fn policy_operation_is_mutation() {
142        assert!(!PolicyOperation::Read.is_mutation());
143        assert!(!PolicyOperation::List.is_mutation());
144        assert!(PolicyOperation::Create.is_mutation());
145        assert!(PolicyOperation::Update.is_mutation());
146        assert!(PolicyOperation::Delete.is_mutation());
147        assert!(PolicyOperation::Command.is_mutation());
148    }
149}