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    pub const fn is_read_only(&self) -> bool {
69        matches!(self, Self::Read | Self::List)
70    }
71
72    /// Check if this is a mutation operation.
73    pub const fn is_mutation(&self) -> bool {
74        !self.is_read_only()
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use std::error::Error;
81
82    use super::*;
83
84    #[derive(Debug)]
85    struct TestError(&'static str);
86
87    impl fmt::Display for TestError {
88        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89            write!(f, "{}", self.0)
90        }
91    }
92
93    impl std::error::Error for TestError {}
94
95    #[test]
96    fn policy_error_is_policy() {
97        let err: PolicyError<TestError, TestError> = PolicyError::Policy(TestError("denied"));
98        assert!(err.is_policy());
99        assert!(!err.is_repository());
100    }
101
102    #[test]
103    fn policy_error_is_repository() {
104        let err: PolicyError<TestError, TestError> = PolicyError::Repository(TestError("db"));
105        assert!(err.is_repository());
106        assert!(!err.is_policy());
107    }
108
109    #[test]
110    fn policy_error_display() {
111        let policy: PolicyError<TestError, TestError> = PolicyError::Policy(TestError("denied"));
112        assert_eq!(format!("{}", policy), "authorization denied: denied");
113
114        let repo: PolicyError<TestError, TestError> = PolicyError::Repository(TestError("db"));
115        assert_eq!(format!("{}", repo), "repository error: db");
116    }
117
118    #[test]
119    fn policy_error_source() {
120        let policy: PolicyError<TestError, TestError> = PolicyError::Policy(TestError("denied"));
121        assert!(policy.source().is_some());
122
123        let repo: PolicyError<TestError, TestError> = PolicyError::Repository(TestError("db"));
124        assert!(repo.source().is_some());
125    }
126
127    #[test]
128    fn policy_operation_is_read_only() {
129        assert!(PolicyOperation::Read.is_read_only());
130        assert!(PolicyOperation::List.is_read_only());
131        assert!(!PolicyOperation::Create.is_read_only());
132        assert!(!PolicyOperation::Update.is_read_only());
133        assert!(!PolicyOperation::Delete.is_read_only());
134        assert!(!PolicyOperation::Command.is_read_only());
135    }
136
137    #[test]
138    fn policy_operation_is_mutation() {
139        assert!(!PolicyOperation::Read.is_mutation());
140        assert!(!PolicyOperation::List.is_mutation());
141        assert!(PolicyOperation::Create.is_mutation());
142        assert!(PolicyOperation::Update.is_mutation());
143        assert!(PolicyOperation::Delete.is_mutation());
144        assert!(PolicyOperation::Command.is_mutation());
145    }
146}