ferro_audit/actor.rs
1//! `AuditActor` — typed actor enum stringly-keyed to keep `ferro-audit`
2//! domain-agnostic (D-05).
3//!
4//! The DB representation is `(actor_kind: String, actor_id: Option<String>)`.
5//! `actor_kind` is the snake_case variant name returned by [`AuditActor::kind`];
6//! `actor_id` is the contained string returned by [`AuditActor::id`], or `NULL`
7//! for `System` and `Anonymous` (no specific identity).
8
9#[derive(Clone, Debug, PartialEq, Eq)]
10pub enum AuditActor {
11 /// Concrete end-user. `String` is consumer-chosen: `i64.to_string()`,
12 /// `Uuid` rendered, slug — anything stable for the consumer.
13 User(String),
14
15 /// Background process with no specific user identity (cron, queue worker,
16 /// system-driven mutation). Persists `actor_id = NULL`.
17 System,
18
19 /// Queued job — the contained string is the job name
20 /// (e.g. `"stripe.webhook.subscription_updated"`).
21 Job(String),
22
23 /// API client — the contained string is the API key id / OAuth client id.
24 ApiClient(String),
25
26 /// Unauthenticated public action (rare but valid). Persists `actor_id = NULL`.
27 Anonymous,
28}
29
30impl AuditActor {
31 /// Returns the snake_case actor kind. Persisted in the `actor_kind` column.
32 pub fn kind(&self) -> &'static str {
33 match self {
34 Self::User(_) => "user",
35 Self::System => "system",
36 Self::Job(_) => "job",
37 Self::ApiClient(_) => "api_client",
38 Self::Anonymous => "anonymous",
39 }
40 }
41
42 /// Returns the actor id, if the variant carries one. `System` and
43 /// `Anonymous` return `None` and persist `actor_id = NULL`.
44 pub fn id(&self) -> Option<&str> {
45 match self {
46 Self::User(id) | Self::Job(id) | Self::ApiClient(id) => Some(id.as_str()),
47 Self::System | Self::Anonymous => None,
48 }
49 }
50}
51
52#[cfg(test)]
53mod tests {
54 use super::*;
55
56 #[test]
57 fn user_kind_and_id() {
58 let a = AuditActor::User("u_42".into());
59 assert_eq!(a.kind(), "user");
60 assert_eq!(a.id(), Some("u_42"));
61 }
62
63 #[test]
64 fn system_kind_and_id() {
65 let a = AuditActor::System;
66 assert_eq!(a.kind(), "system");
67 assert_eq!(a.id(), None);
68 }
69
70 #[test]
71 fn job_kind_and_id() {
72 let a = AuditActor::Job("stripe.webhook.subscription_updated".into());
73 assert_eq!(a.kind(), "job");
74 assert_eq!(a.id(), Some("stripe.webhook.subscription_updated"));
75 }
76
77 #[test]
78 fn api_client_kind_and_id() {
79 let a = AuditActor::ApiClient("oauth_client_xyz".into());
80 assert_eq!(a.kind(), "api_client");
81 assert_eq!(a.id(), Some("oauth_client_xyz"));
82 }
83
84 #[test]
85 fn anonymous_kind_and_id() {
86 let a = AuditActor::Anonymous;
87 assert_eq!(a.kind(), "anonymous");
88 assert_eq!(a.id(), None);
89 }
90}