Skip to main content

wami_core/
actions.rs

1//! WAMI Action vocabulary — the complete set of permissions for the obrain ecosystem.
2//!
3//! Each action follows the `service:Operation` convention (e.g. `"db:Query"`, `"tenant:Create"`).
4//! Special wildcards:
5//! - `"*"` matches ALL actions
6//! - `"service:*"` matches all actions within a service prefix
7
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9use std::fmt;
10use std::str::FromStr;
11use std::sync::LazyLock;
12
13// ---------------------------------------------------------------------------
14// WamiAction enum
15// ---------------------------------------------------------------------------
16
17/// Exhaustive action vocabulary for the obrain ecosystem.
18///
19/// Actions are organized by service prefix:
20/// - `platform:*` — Platform-level administration
21/// - `tenant:*` — Tenant hierarchy management
22/// - `iam:*` — Identity & access management
23/// - `db:*` — Database / knowledge store operations
24/// - `chat:*` — Chat and conversation
25/// - `persona:*` — Persona management
26/// - `room:*` — Room / channel management
27/// - `inference:*` — Model and inference operations
28/// - `analytics:*` — Analytics and reporting
29/// - `integration:*` — External platform integrations
30/// - `cognitive:*` — Cognitive layer (brain, memory)
31/// - `gdpr:*` — GDPR / data privacy operations
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
33pub enum WamiAction {
34    // -- Wildcards ----------------------------------------------------------
35    /// Matches every action (`*`)
36    All,
37    /// Matches every action within a service prefix (`service:*`)
38    ServiceAll(WamiServicePrefix),
39
40    // -- platform -----------------------------------------------------------
41    /// Full platform administration
42    PlatformAdmin,
43    /// View platform settings and stats
44    PlatformViewSettings,
45    /// Update platform configuration
46    PlatformUpdateSettings,
47    /// View platform audit logs
48    PlatformViewAuditLog,
49
50    // -- tenant -------------------------------------------------------------
51    /// Create a new tenant
52    TenantCreate,
53    /// Configure tenant domain and port
54    TenantConfigure,
55    /// Manage tenant members (add, remove, ban)
56    TenantManageMembers,
57    /// Suspend a tenant
58    ///
59    /// Its opposite is `TenantResume` and not this: cutting access off and
60    /// giving it back are not one permission. A tenant is usually suspended
61    /// FOR a reason — unpaid, abusive, breached — and whoever may cut it off is
62    /// not automatically whoever may decide the reason has passed.
63    TenantSuspend,
64    /// Lift a suspension
65    TenantResume,
66    /// List all tenants (platform-level)
67    TenantList,
68
69    // -- tenant -------------------------------------------------------------
70    /// Read tenant info
71    TenantRead,
72    /// Update tenant
73    TenantUpdate,
74    /// Delete tenant
75    TenantDelete,
76    /// Create sub-tenant
77    TenantCreateSubTenant,
78    /// Manage users within tenant
79    TenantManageUsers,
80    /// Manage roles within tenant
81    TenantManageRoles,
82    /// Manage policies within tenant
83    TenantManagePolicies,
84
85    // -- iam ----------------------------------------------------------------
86    /// Create IAM user
87    IamCreateUser,
88    /// Delete IAM user
89    IamDeleteUser,
90    /// Read IAM user info
91    IamReadUser,
92    /// Update IAM user
93    IamUpdateUser,
94    /// List IAM users
95    IamListUsers,
96    /// Create IAM group
97    IamCreateGroup,
98    /// Delete IAM group
99    IamDeleteGroup,
100    /// Manage group membership
101    IamManageGroupMembers,
102    /// Create IAM role
103    IamCreateRole,
104    /// Delete IAM role
105    IamDeleteRole,
106    /// Read IAM role
107    IamReadRole,
108    /// Assume IAM role
109    IamAssumeRole,
110    /// Create an IAM policy
111    ///
112    /// Attaching it is `IamAttachPolicy`, which exists. Saying both here would
113    /// make that one grant nothing anybody could not already do.
114    IamCreatePolicy,
115    /// Delete an IAM policy
116    ///
117    /// Detaching it is `IamDetachPolicy`, for the same reason.
118    IamDeletePolicy,
119    /// Read IAM policy
120    IamReadPolicy,
121    /// Attach policy to user/group/role
122    IamAttachPolicy,
123    /// Detach policy from user/group/role
124    IamDetachPolicy,
125    /// Set permissions boundary
126    IamSetBoundary,
127    /// Manage credentials (access keys, MFA, etc.)
128    IamManageCredentials,
129
130    // -- db -----------------------------------------------------------------
131    /// Query / read from a knowledge database
132    DbQuery,
133    /// Write / insert into a knowledge database
134    DbWrite,
135    /// Delete data from a knowledge database
136    DbDelete,
137    /// Create a new knowledge database
138    DbCreate,
139    /// Drop a knowledge database
140    DbDrop,
141    /// List available databases
142    DbList,
143    /// Configure database access policies
144    DbConfigureAccess,
145    /// Import data into a database
146    DbImport,
147    /// Export data from a database
148    DbExport,
149
150    // -- chat ---------------------------------------------------------------
151    /// Send a chat message
152    ChatSend,
153    /// Read chat history
154    ChatReadHistory,
155    /// Delete a conversation
156    ChatDeleteConversation,
157    /// Use streaming chat (SSE)
158    ChatStream,
159
160    // -- persona ------------------------------------------------------------
161    /// Create a persona
162    PersonaCreate,
163    /// Delete a persona
164    PersonaDelete,
165    /// Read persona info
166    PersonaRead,
167    /// Update persona configuration
168    PersonaUpdate,
169    /// List personas
170    PersonaList,
171    /// Invoke / use a persona in chat
172    PersonaInvoke,
173
174    // -- room ---------------------------------------------------------------
175    /// Create a room / channel
176    RoomCreate,
177    /// Delete a room
178    RoomDelete,
179    /// Join a room
180    RoomJoin,
181    /// Read room messages
182    RoomRead,
183    /// Send message to room
184    RoomSend,
185    /// Manage room settings
186    RoomManage,
187
188    // -- inference ----------------------------------------------------------
189    /// List available models
190    InferenceListModels,
191    /// Invoke a model (generate)
192    InferenceInvoke,
193    /// Configure model routing
194    InferenceConfigureRouter,
195    /// View model usage / costs
196    InferenceViewUsage,
197
198    // -- analytics ----------------------------------------------------------
199    /// View usage analytics
200    AnalyticsViewUsage,
201    /// View conversation analytics
202    AnalyticsViewConversations,
203    /// Export analytics reports
204    AnalyticsExport,
205    /// View user activity
206    AnalyticsViewActivity,
207
208    // -- integration --------------------------------------------------------
209    /// Create an integration bridge
210    IntegrationCreate,
211    /// Delete an integration bridge
212    IntegrationDelete,
213    /// Configure integration settings
214    IntegrationConfigure,
215    /// List integrations
216    IntegrationList,
217    /// Receive inbound messages (webhook)
218    IntegrationReceive,
219    /// Send outbound messages
220    IntegrationSend,
221
222    // -- cognitive ----------------------------------------------------------
223    /// Read cognitive state (brain, memory)
224    CognitiveRead,
225    /// Write / update cognitive state
226    CognitiveWrite,
227    /// Reset cognitive state
228    CognitiveReset,
229
230    // -- gdpr ---------------------------------------------------------------
231    /// Grant data consent
232    GdprGrantConsent,
233    /// Revoke data consent
234    GdprRevokeConsent,
235    /// Export personal data
236    GdprExportData,
237    /// Erase personal data (right to be forgotten)
238    GdprEraseData,
239    /// View audit log
240    GdprViewAudit,
241}
242
243// ---------------------------------------------------------------------------
244// Service prefixes
245// ---------------------------------------------------------------------------
246
247/// Service prefix for `ServiceAll` wildcard matching.
248#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
249pub enum WamiServicePrefix {
250    Platform,
251    Tenant,
252    Iam,
253    Db,
254    Chat,
255    Persona,
256    Room,
257    Inference,
258    Analytics,
259    Integration,
260    Cognitive,
261    Gdpr,
262}
263
264impl WamiServicePrefix {
265    pub fn as_str(&self) -> &'static str {
266        match self {
267            Self::Platform => "platform",
268            Self::Tenant => "tenant",
269            Self::Iam => "iam",
270            Self::Db => "db",
271            Self::Chat => "chat",
272            Self::Persona => "persona",
273            Self::Room => "room",
274            Self::Inference => "inference",
275            Self::Analytics => "analytics",
276            Self::Integration => "integration",
277            Self::Cognitive => "cognitive",
278            Self::Gdpr => "gdpr",
279        }
280    }
281
282    pub fn all() -> &'static [WamiServicePrefix] {
283        &[
284            Self::Platform,
285            Self::Tenant,
286            Self::Iam,
287            Self::Db,
288            Self::Chat,
289            Self::Persona,
290            Self::Room,
291            Self::Inference,
292            Self::Analytics,
293            Self::Integration,
294            Self::Cognitive,
295            Self::Gdpr,
296        ]
297    }
298}
299
300impl FromStr for WamiServicePrefix {
301    type Err = ActionParseError;
302
303    fn from_str(s: &str) -> Result<Self, Self::Err> {
304        match s {
305            "platform" => Ok(Self::Platform),
306            "tenant" => Ok(Self::Tenant),
307            "iam" => Ok(Self::Iam),
308            "db" => Ok(Self::Db),
309            "chat" => Ok(Self::Chat),
310            "persona" => Ok(Self::Persona),
311            "room" => Ok(Self::Room),
312            "inference" => Ok(Self::Inference),
313            "analytics" => Ok(Self::Analytics),
314            "integration" => Ok(Self::Integration),
315            "cognitive" => Ok(Self::Cognitive),
316            "gdpr" => Ok(Self::Gdpr),
317            _ => Err(ActionParseError::UnknownPrefix(s.to_string())),
318        }
319    }
320}
321
322impl fmt::Display for WamiServicePrefix {
323    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
324        f.write_str(self.as_str())
325    }
326}
327
328// ---------------------------------------------------------------------------
329// Display / FromStr — "service:Operation" format
330// ---------------------------------------------------------------------------
331
332impl fmt::Display for WamiAction {
333    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
334        f.write_str(self.as_str())
335    }
336}
337
338impl WamiAction {
339    /// String representation in `service:Operation` format.
340    pub fn as_str(&self) -> &'static str {
341        match self {
342            // Wildcards
343            Self::All => "*",
344            Self::ServiceAll(prefix) => match prefix {
345                WamiServicePrefix::Platform => "platform:*",
346                WamiServicePrefix::Tenant => "tenant:*",
347                WamiServicePrefix::Iam => "iam:*",
348                WamiServicePrefix::Db => "db:*",
349                WamiServicePrefix::Chat => "chat:*",
350                WamiServicePrefix::Persona => "persona:*",
351                WamiServicePrefix::Room => "room:*",
352                WamiServicePrefix::Inference => "inference:*",
353                WamiServicePrefix::Analytics => "analytics:*",
354                WamiServicePrefix::Integration => "integration:*",
355                WamiServicePrefix::Cognitive => "cognitive:*",
356                WamiServicePrefix::Gdpr => "gdpr:*",
357            },
358            // Platform
359            Self::PlatformAdmin => "platform:Admin",
360            Self::PlatformViewSettings => "platform:ViewSettings",
361            Self::PlatformUpdateSettings => "platform:UpdateSettings",
362            Self::PlatformViewAuditLog => "platform:ViewAuditLog",
363            Self::TenantCreate => "tenant:Create",
364            Self::TenantConfigure => "tenant:Configure",
365            Self::TenantManageMembers => "tenant:ManageMembers",
366            Self::TenantSuspend => "tenant:Suspend",
367            Self::TenantResume => "tenant:Resume",
368            Self::TenantList => "tenant:List",
369            // Tenant
370            Self::TenantRead => "tenant:Read",
371            Self::TenantUpdate => "tenant:Update",
372            Self::TenantDelete => "tenant:Delete",
373            Self::TenantCreateSubTenant => "tenant:CreateSubTenant",
374            Self::TenantManageUsers => "tenant:ManageUsers",
375            Self::TenantManageRoles => "tenant:ManageRoles",
376            Self::TenantManagePolicies => "tenant:ManagePolicies",
377            // IAM
378            Self::IamCreateUser => "iam:CreateUser",
379            Self::IamDeleteUser => "iam:DeleteUser",
380            Self::IamReadUser => "iam:ReadUser",
381            Self::IamUpdateUser => "iam:UpdateUser",
382            Self::IamListUsers => "iam:ListUsers",
383            Self::IamCreateGroup => "iam:CreateGroup",
384            Self::IamDeleteGroup => "iam:DeleteGroup",
385            Self::IamManageGroupMembers => "iam:ManageGroupMembers",
386            Self::IamCreateRole => "iam:CreateRole",
387            Self::IamDeleteRole => "iam:DeleteRole",
388            Self::IamReadRole => "iam:ReadRole",
389            Self::IamAssumeRole => "iam:AssumeRole",
390            Self::IamCreatePolicy => "iam:CreatePolicy",
391            Self::IamDeletePolicy => "iam:DeletePolicy",
392            Self::IamReadPolicy => "iam:ReadPolicy",
393            Self::IamAttachPolicy => "iam:AttachPolicy",
394            Self::IamDetachPolicy => "iam:DetachPolicy",
395            Self::IamSetBoundary => "iam:SetBoundary",
396            Self::IamManageCredentials => "iam:ManageCredentials",
397            // DB
398            Self::DbQuery => "db:Query",
399            Self::DbWrite => "db:Write",
400            Self::DbDelete => "db:Delete",
401            Self::DbCreate => "db:Create",
402            Self::DbDrop => "db:Drop",
403            Self::DbList => "db:List",
404            Self::DbConfigureAccess => "db:ConfigureAccess",
405            Self::DbImport => "db:Import",
406            Self::DbExport => "db:Export",
407            // Chat
408            Self::ChatSend => "chat:Send",
409            Self::ChatReadHistory => "chat:ReadHistory",
410            Self::ChatDeleteConversation => "chat:DeleteConversation",
411            Self::ChatStream => "chat:Stream",
412            // Persona
413            Self::PersonaCreate => "persona:Create",
414            Self::PersonaDelete => "persona:Delete",
415            Self::PersonaRead => "persona:Read",
416            Self::PersonaUpdate => "persona:Update",
417            Self::PersonaList => "persona:List",
418            Self::PersonaInvoke => "persona:Invoke",
419            // Room
420            Self::RoomCreate => "room:Create",
421            Self::RoomDelete => "room:Delete",
422            Self::RoomJoin => "room:Join",
423            Self::RoomRead => "room:Read",
424            Self::RoomSend => "room:Send",
425            Self::RoomManage => "room:Manage",
426            // Inference
427            Self::InferenceListModels => "inference:ListModels",
428            Self::InferenceInvoke => "inference:Invoke",
429            Self::InferenceConfigureRouter => "inference:ConfigureRouter",
430            Self::InferenceViewUsage => "inference:ViewUsage",
431            // Analytics
432            Self::AnalyticsViewUsage => "analytics:ViewUsage",
433            Self::AnalyticsViewConversations => "analytics:ViewConversations",
434            Self::AnalyticsExport => "analytics:Export",
435            Self::AnalyticsViewActivity => "analytics:ViewActivity",
436            // Integration
437            Self::IntegrationCreate => "integration:Create",
438            Self::IntegrationDelete => "integration:Delete",
439            Self::IntegrationConfigure => "integration:Configure",
440            Self::IntegrationList => "integration:List",
441            Self::IntegrationReceive => "integration:Receive",
442            Self::IntegrationSend => "integration:Send",
443            // Cognitive
444            Self::CognitiveRead => "cognitive:Read",
445            Self::CognitiveWrite => "cognitive:Write",
446            Self::CognitiveReset => "cognitive:Reset",
447            // GDPR
448            Self::GdprGrantConsent => "gdpr:GrantConsent",
449            Self::GdprRevokeConsent => "gdpr:RevokeConsent",
450            Self::GdprExportData => "gdpr:ExportData",
451            Self::GdprEraseData => "gdpr:EraseData",
452            Self::GdprViewAudit => "gdpr:ViewAudit",
453        }
454    }
455
456    /// Returns the service prefix for this action.
457    pub fn prefix(&self) -> Option<WamiServicePrefix> {
458        match self {
459            Self::All => None,
460            Self::ServiceAll(p) => Some(*p),
461            Self::PlatformAdmin
462            | Self::PlatformViewSettings
463            | Self::PlatformUpdateSettings
464            | Self::PlatformViewAuditLog => Some(WamiServicePrefix::Platform),
465            Self::TenantCreate
466            | Self::TenantConfigure
467            | Self::TenantManageMembers
468            | Self::TenantSuspend
469            | Self::TenantResume
470            | Self::TenantList
471            | Self::TenantRead
472            | Self::TenantUpdate
473            | Self::TenantDelete
474            | Self::TenantCreateSubTenant
475            | Self::TenantManageUsers
476            | Self::TenantManageRoles
477            | Self::TenantManagePolicies => Some(WamiServicePrefix::Tenant),
478            Self::IamCreateUser
479            | Self::IamDeleteUser
480            | Self::IamReadUser
481            | Self::IamUpdateUser
482            | Self::IamListUsers
483            | Self::IamCreateGroup
484            | Self::IamDeleteGroup
485            | Self::IamManageGroupMembers
486            | Self::IamCreateRole
487            | Self::IamDeleteRole
488            | Self::IamReadRole
489            | Self::IamAssumeRole
490            | Self::IamCreatePolicy
491            | Self::IamDeletePolicy
492            | Self::IamReadPolicy
493            | Self::IamAttachPolicy
494            | Self::IamDetachPolicy
495            | Self::IamSetBoundary
496            | Self::IamManageCredentials => Some(WamiServicePrefix::Iam),
497            Self::DbQuery
498            | Self::DbWrite
499            | Self::DbDelete
500            | Self::DbCreate
501            | Self::DbDrop
502            | Self::DbList
503            | Self::DbConfigureAccess
504            | Self::DbImport
505            | Self::DbExport => Some(WamiServicePrefix::Db),
506            Self::ChatSend
507            | Self::ChatReadHistory
508            | Self::ChatDeleteConversation
509            | Self::ChatStream => Some(WamiServicePrefix::Chat),
510            Self::PersonaCreate
511            | Self::PersonaDelete
512            | Self::PersonaRead
513            | Self::PersonaUpdate
514            | Self::PersonaList
515            | Self::PersonaInvoke => Some(WamiServicePrefix::Persona),
516            Self::RoomCreate
517            | Self::RoomDelete
518            | Self::RoomJoin
519            | Self::RoomRead
520            | Self::RoomSend
521            | Self::RoomManage => Some(WamiServicePrefix::Room),
522            Self::InferenceListModels
523            | Self::InferenceInvoke
524            | Self::InferenceConfigureRouter
525            | Self::InferenceViewUsage => Some(WamiServicePrefix::Inference),
526            Self::AnalyticsViewUsage
527            | Self::AnalyticsViewConversations
528            | Self::AnalyticsExport
529            | Self::AnalyticsViewActivity => Some(WamiServicePrefix::Analytics),
530            Self::IntegrationCreate
531            | Self::IntegrationDelete
532            | Self::IntegrationConfigure
533            | Self::IntegrationList
534            | Self::IntegrationReceive
535            | Self::IntegrationSend => Some(WamiServicePrefix::Integration),
536            Self::CognitiveRead | Self::CognitiveWrite | Self::CognitiveReset => {
537                Some(WamiServicePrefix::Cognitive)
538            }
539            Self::GdprGrantConsent
540            | Self::GdprRevokeConsent
541            | Self::GdprExportData
542            | Self::GdprEraseData
543            | Self::GdprViewAudit => Some(WamiServicePrefix::Gdpr),
544        }
545    }
546}
547
548// ---------------------------------------------------------------------------
549// Parse error
550// ---------------------------------------------------------------------------
551
552#[derive(Debug, Clone, thiserror::Error)]
553pub enum ActionParseError {
554    #[error("unknown action: {0}")]
555    Unknown(String),
556    #[error("unknown service prefix: {0}")]
557    UnknownPrefix(String),
558    #[error("invalid action format (expected 'service:Operation' or '*'): {0}")]
559    InvalidFormat(String),
560}
561
562// ---------------------------------------------------------------------------
563// FromStr
564// ---------------------------------------------------------------------------
565
566impl FromStr for WamiAction {
567    type Err = ActionParseError;
568
569    fn from_str(s: &str) -> Result<Self, Self::Err> {
570        // Global wildcard
571        if s == "*" {
572            return Ok(Self::All);
573        }
574
575        // Must contain ':'
576        let Some((prefix_str, operation)) = s.split_once(':') else {
577            return Err(ActionParseError::InvalidFormat(s.to_string()));
578        };
579
580        // Service wildcard
581        if operation == "*" {
582            let prefix = WamiServicePrefix::from_str(prefix_str)?;
583            return Ok(Self::ServiceAll(prefix));
584        }
585
586        // Exact match
587        match (prefix_str, operation) {
588            // Platform
589            ("platform", "Admin") => Ok(Self::PlatformAdmin),
590            ("platform", "ViewSettings") => Ok(Self::PlatformViewSettings),
591            ("platform", "UpdateSettings") => Ok(Self::PlatformUpdateSettings),
592            ("platform", "ViewAuditLog") => Ok(Self::PlatformViewAuditLog),
593            // Tenant
594            ("tenant", "Create") => Ok(Self::TenantCreate),
595            ("tenant", "Configure") => Ok(Self::TenantConfigure),
596            ("tenant", "ManageMembers") => Ok(Self::TenantManageMembers),
597            ("tenant", "Suspend") => Ok(Self::TenantSuspend),
598            ("tenant", "Resume") => Ok(Self::TenantResume),
599            ("tenant", "List") => Ok(Self::TenantList),
600            ("tenant", "Read") => Ok(Self::TenantRead),
601            ("tenant", "Update") => Ok(Self::TenantUpdate),
602            ("tenant", "Delete") => Ok(Self::TenantDelete),
603            ("tenant", "CreateSubTenant") => Ok(Self::TenantCreateSubTenant),
604            ("tenant", "ManageUsers") => Ok(Self::TenantManageUsers),
605            ("tenant", "ManageRoles") => Ok(Self::TenantManageRoles),
606            ("tenant", "ManagePolicies") => Ok(Self::TenantManagePolicies),
607            // IAM
608            ("iam", "CreateUser") => Ok(Self::IamCreateUser),
609            ("iam", "DeleteUser") => Ok(Self::IamDeleteUser),
610            ("iam", "ReadUser") => Ok(Self::IamReadUser),
611            ("iam", "UpdateUser") => Ok(Self::IamUpdateUser),
612            ("iam", "ListUsers") => Ok(Self::IamListUsers),
613            ("iam", "CreateGroup") => Ok(Self::IamCreateGroup),
614            ("iam", "DeleteGroup") => Ok(Self::IamDeleteGroup),
615            ("iam", "ManageGroupMembers") => Ok(Self::IamManageGroupMembers),
616            ("iam", "CreateRole") => Ok(Self::IamCreateRole),
617            ("iam", "DeleteRole") => Ok(Self::IamDeleteRole),
618            ("iam", "ReadRole") => Ok(Self::IamReadRole),
619            ("iam", "AssumeRole") => Ok(Self::IamAssumeRole),
620            ("iam", "CreatePolicy") => Ok(Self::IamCreatePolicy),
621            ("iam", "DeletePolicy") => Ok(Self::IamDeletePolicy),
622            ("iam", "ReadPolicy") => Ok(Self::IamReadPolicy),
623            ("iam", "AttachPolicy") => Ok(Self::IamAttachPolicy),
624            ("iam", "DetachPolicy") => Ok(Self::IamDetachPolicy),
625            ("iam", "SetBoundary") => Ok(Self::IamSetBoundary),
626            ("iam", "ManageCredentials") => Ok(Self::IamManageCredentials),
627            // DB
628            ("db", "Query") => Ok(Self::DbQuery),
629            ("db", "Write") => Ok(Self::DbWrite),
630            ("db", "Delete") => Ok(Self::DbDelete),
631            ("db", "Create") => Ok(Self::DbCreate),
632            ("db", "Drop") => Ok(Self::DbDrop),
633            ("db", "List") => Ok(Self::DbList),
634            ("db", "ConfigureAccess") => Ok(Self::DbConfigureAccess),
635            ("db", "Import") => Ok(Self::DbImport),
636            ("db", "Export") => Ok(Self::DbExport),
637            // Chat
638            ("chat", "Send") => Ok(Self::ChatSend),
639            ("chat", "ReadHistory") => Ok(Self::ChatReadHistory),
640            ("chat", "DeleteConversation") => Ok(Self::ChatDeleteConversation),
641            ("chat", "Stream") => Ok(Self::ChatStream),
642            // Persona
643            ("persona", "Create") => Ok(Self::PersonaCreate),
644            ("persona", "Delete") => Ok(Self::PersonaDelete),
645            ("persona", "Read") => Ok(Self::PersonaRead),
646            ("persona", "Update") => Ok(Self::PersonaUpdate),
647            ("persona", "List") => Ok(Self::PersonaList),
648            ("persona", "Invoke") => Ok(Self::PersonaInvoke),
649            // Room
650            ("room", "Create") => Ok(Self::RoomCreate),
651            ("room", "Delete") => Ok(Self::RoomDelete),
652            ("room", "Join") => Ok(Self::RoomJoin),
653            ("room", "Read") => Ok(Self::RoomRead),
654            ("room", "Send") => Ok(Self::RoomSend),
655            ("room", "Manage") => Ok(Self::RoomManage),
656            // Inference
657            ("inference", "ListModels") => Ok(Self::InferenceListModels),
658            ("inference", "Invoke") => Ok(Self::InferenceInvoke),
659            ("inference", "ConfigureRouter") => Ok(Self::InferenceConfigureRouter),
660            ("inference", "ViewUsage") => Ok(Self::InferenceViewUsage),
661            // Analytics
662            ("analytics", "ViewUsage") => Ok(Self::AnalyticsViewUsage),
663            ("analytics", "ViewConversations") => Ok(Self::AnalyticsViewConversations),
664            ("analytics", "Export") => Ok(Self::AnalyticsExport),
665            ("analytics", "ViewActivity") => Ok(Self::AnalyticsViewActivity),
666            // Integration
667            ("integration", "Create") => Ok(Self::IntegrationCreate),
668            ("integration", "Delete") => Ok(Self::IntegrationDelete),
669            ("integration", "Configure") => Ok(Self::IntegrationConfigure),
670            ("integration", "List") => Ok(Self::IntegrationList),
671            ("integration", "Receive") => Ok(Self::IntegrationReceive),
672            ("integration", "Send") => Ok(Self::IntegrationSend),
673            // Cognitive
674            ("cognitive", "Read") => Ok(Self::CognitiveRead),
675            ("cognitive", "Write") => Ok(Self::CognitiveWrite),
676            ("cognitive", "Reset") => Ok(Self::CognitiveReset),
677            // GDPR
678            ("gdpr", "GrantConsent") => Ok(Self::GdprGrantConsent),
679            ("gdpr", "RevokeConsent") => Ok(Self::GdprRevokeConsent),
680            ("gdpr", "ExportData") => Ok(Self::GdprExportData),
681            ("gdpr", "EraseData") => Ok(Self::GdprEraseData),
682            ("gdpr", "ViewAudit") => Ok(Self::GdprViewAudit),
683            _ => Err(ActionParseError::Unknown(s.to_string())),
684        }
685    }
686}
687
688// ---------------------------------------------------------------------------
689// Serde — serialize as string
690// ---------------------------------------------------------------------------
691
692impl Serialize for WamiAction {
693    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
694        serializer.serialize_str(self.as_str())
695    }
696}
697
698impl<'de> Deserialize<'de> for WamiAction {
699    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
700        let s = String::deserialize(deserializer)?;
701        WamiAction::from_str(&s).map_err(serde::de::Error::custom)
702    }
703}
704
705// ---------------------------------------------------------------------------
706// Wildcard matching
707// ---------------------------------------------------------------------------
708
709impl WamiAction {
710    /// Check whether `self` (a policy action pattern) matches a `requested` action.
711    ///
712    /// Matching rules:
713    /// - `All` (`*`) matches everything
714    /// - `ServiceAll(prefix)` (`service:*`) matches any action in that service
715    /// - Exact variant matches only itself
716    ///
717    /// This also supports **string-based** matching for backward compatibility
718    /// with existing `Vec<String>` policy statements.
719    pub fn matches(&self, requested: &WamiAction) -> bool {
720        match self {
721            // Global wildcard matches everything
722            Self::All => true,
723            // Service wildcard matches anything in the same prefix
724            Self::ServiceAll(prefix) => requested.prefix() == Some(*prefix),
725            // Exact match
726            other => other == requested,
727        }
728    }
729
730    /// Check whether a policy action **string** matches a requested action string.
731    ///
732    /// This provides backward compatibility with the existing `Vec<String>` in
733    /// `PolicyStatement` without requiring a full migration to the enum.
734    ///
735    /// Supports: `"*"`, `"service:*"`, and exact strings like `"db:Query"`.
736    pub fn matches_str(policy_action: &str, requested: &str) -> bool {
737        if policy_action == "*" {
738            return true;
739        }
740        if policy_action == requested {
741            return true;
742        }
743        // Prefix wildcard: "iam:*" matches "iam:CreateUser"
744        if let Some(prefix) = policy_action.strip_suffix(":*") {
745            if let Some(req_prefix) = requested.split_once(':').map(|(p, _)| p) {
746                return prefix == req_prefix;
747            }
748        }
749        false
750    }
751}
752
753// ---------------------------------------------------------------------------
754// ActionRegistry — metadata for UI consumption
755// ---------------------------------------------------------------------------
756
757/// Metadata about a single action, for UI display and policy builders.
758#[derive(Debug, Clone, Serialize, Deserialize)]
759pub struct ActionInfo {
760    /// The action string (e.g. `"db:Query"`)
761    pub action: String,
762    /// Human-readable description
763    pub description: String,
764    /// Service category (e.g. `"db"`, `"iam"`)
765    pub category: String,
766}
767
768/// Registry of all known actions with their metadata.
769pub struct ActionRegistry;
770
771impl ActionRegistry {
772    /// Return all registered actions with their descriptions.
773    pub fn list_actions() -> &'static [ActionInfo] {
774        &REGISTRY
775    }
776
777    /// List actions filtered by service prefix.
778    pub fn list_by_prefix(prefix: WamiServicePrefix) -> Vec<&'static ActionInfo> {
779        let prefix_str = prefix.as_str();
780        REGISTRY
781            .iter()
782            .filter(|info| info.category == prefix_str)
783            .collect()
784    }
785
786    /// List all service prefixes.
787    pub fn list_prefixes() -> &'static [WamiServicePrefix] {
788        WamiServicePrefix::all()
789    }
790}
791
792// The static registry, built once.
793static REGISTRY: LazyLock<Vec<ActionInfo>> = LazyLock::new(|| {
794    vec![
795        // Platform
796        ai("platform:Admin", "Full platform administration", "platform"),
797        ai(
798            "platform:ViewSettings",
799            "View platform settings and stats",
800            "platform",
801        ),
802        ai(
803            "platform:UpdateSettings",
804            "Update platform configuration",
805            "platform",
806        ),
807        ai(
808            "platform:ViewAuditLog",
809            "View platform audit logs",
810            "platform",
811        ),
812        // Tenant
813        ai("tenant:Create", "Create a new tenant", "tenant"),
814        ai(
815            "tenant:Configure",
816            "Configure tenant domain and port",
817            "tenant",
818        ),
819        ai("tenant:ManageMembers", "Manage tenant members", "tenant"),
820        ai("tenant:Suspend", "Suspend a tenant", "tenant"),
821        ai("tenant:Resume", "Lift a tenant suspension", "tenant"),
822        ai("tenant:List", "List all tenants (platform-level)", "tenant"),
823        ai("tenant:Read", "Read tenant info", "tenant"),
824        ai("tenant:Update", "Update tenant", "tenant"),
825        ai("tenant:Delete", "Delete tenant", "tenant"),
826        ai("tenant:CreateSubTenant", "Create sub-tenant", "tenant"),
827        ai("tenant:ManageUsers", "Manage users within tenant", "tenant"),
828        ai("tenant:ManageRoles", "Manage roles within tenant", "tenant"),
829        ai(
830            "tenant:ManagePolicies",
831            "Manage policies within tenant",
832            "tenant",
833        ),
834        // IAM
835        ai("iam:CreateUser", "Create IAM user", "iam"),
836        ai("iam:DeleteUser", "Delete IAM user", "iam"),
837        ai("iam:ReadUser", "Read IAM user info", "iam"),
838        ai("iam:UpdateUser", "Update IAM user", "iam"),
839        ai("iam:ListUsers", "List IAM users", "iam"),
840        ai("iam:CreateGroup", "Create IAM group", "iam"),
841        ai("iam:DeleteGroup", "Delete IAM group", "iam"),
842        ai("iam:ManageGroupMembers", "Manage group membership", "iam"),
843        ai("iam:CreateRole", "Create IAM role", "iam"),
844        ai("iam:DeleteRole", "Delete IAM role", "iam"),
845        ai("iam:ReadRole", "Read IAM role", "iam"),
846        ai("iam:AssumeRole", "Assume IAM role", "iam"),
847        ai("iam:CreatePolicy", "Create IAM policy", "iam"),
848        ai("iam:DeletePolicy", "Delete IAM policy", "iam"),
849        ai("iam:ReadPolicy", "Read IAM policy", "iam"),
850        ai("iam:AttachPolicy", "Attach policy to entity", "iam"),
851        ai("iam:DetachPolicy", "Detach policy from entity", "iam"),
852        ai("iam:SetBoundary", "Set permissions boundary", "iam"),
853        ai("iam:ManageCredentials", "Manage credentials", "iam"),
854        // DB
855        ai("db:Query", "Query a knowledge database", "db"),
856        ai("db:Write", "Write to a knowledge database", "db"),
857        ai("db:Delete", "Delete data from database", "db"),
858        ai("db:Create", "Create a knowledge database", "db"),
859        ai("db:Drop", "Drop a knowledge database", "db"),
860        ai("db:List", "List available databases", "db"),
861        ai("db:ConfigureAccess", "Configure database access", "db"),
862        ai("db:Import", "Import data into database", "db"),
863        ai("db:Export", "Export data from database", "db"),
864        // Chat
865        ai("chat:Send", "Send a chat message", "chat"),
866        ai("chat:ReadHistory", "Read chat history", "chat"),
867        ai("chat:DeleteConversation", "Delete a conversation", "chat"),
868        ai("chat:Stream", "Use streaming chat (SSE)", "chat"),
869        // Persona
870        ai("persona:Create", "Create a persona", "persona"),
871        ai("persona:Delete", "Delete a persona", "persona"),
872        ai("persona:Read", "Read persona info", "persona"),
873        ai("persona:Update", "Update persona configuration", "persona"),
874        ai("persona:List", "List personas", "persona"),
875        ai("persona:Invoke", "Invoke a persona in chat", "persona"),
876        // Room
877        ai("room:Create", "Create a room", "room"),
878        ai("room:Delete", "Delete a room", "room"),
879        ai("room:Join", "Join a room", "room"),
880        ai("room:Read", "Read room messages", "room"),
881        ai("room:Send", "Send message to room", "room"),
882        ai("room:Manage", "Manage room settings", "room"),
883        // Inference
884        ai("inference:ListModels", "List available models", "inference"),
885        ai("inference:Invoke", "Invoke a model", "inference"),
886        ai(
887            "inference:ConfigureRouter",
888            "Configure model routing",
889            "inference",
890        ),
891        ai("inference:ViewUsage", "View model usage", "inference"),
892        // Analytics
893        ai("analytics:ViewUsage", "View usage analytics", "analytics"),
894        ai(
895            "analytics:ViewConversations",
896            "View conversation analytics",
897            "analytics",
898        ),
899        ai("analytics:Export", "Export analytics reports", "analytics"),
900        ai("analytics:ViewActivity", "View user activity", "analytics"),
901        // Integration
902        ai(
903            "integration:Create",
904            "Create integration bridge",
905            "integration",
906        ),
907        ai(
908            "integration:Delete",
909            "Delete integration bridge",
910            "integration",
911        ),
912        ai(
913            "integration:Configure",
914            "Configure integration",
915            "integration",
916        ),
917        ai("integration:List", "List integrations", "integration"),
918        ai(
919            "integration:Receive",
920            "Receive inbound messages",
921            "integration",
922        ),
923        ai("integration:Send", "Send outbound messages", "integration"),
924        // Cognitive
925        ai("cognitive:Read", "Read cognitive state", "cognitive"),
926        ai("cognitive:Write", "Write cognitive state", "cognitive"),
927        ai("cognitive:Reset", "Reset cognitive state", "cognitive"),
928        // GDPR
929        ai("gdpr:GrantConsent", "Grant data consent", "gdpr"),
930        ai("gdpr:RevokeConsent", "Revoke data consent", "gdpr"),
931        ai("gdpr:ExportData", "Export personal data", "gdpr"),
932        ai("gdpr:EraseData", "Erase personal data", "gdpr"),
933        ai("gdpr:ViewAudit", "View GDPR audit log", "gdpr"),
934    ]
935});
936
937/// Helper to construct an [`ActionInfo`].
938fn ai(action: &str, description: &str, category: &str) -> ActionInfo {
939    ActionInfo {
940        action: action.to_string(),
941        description: description.to_string(),
942        category: category.to_string(),
943    }
944}
945
946// ---------------------------------------------------------------------------
947// Tests
948// ---------------------------------------------------------------------------
949
950#[cfg(test)]
951mod tests {
952    use super::*;
953
954    #[test]
955    fn serde_roundtrip() {
956        let action = WamiAction::DbQuery;
957        let json = serde_json::to_string(&action).unwrap();
958        assert_eq!(json, r#""db:Query""#);
959
960        let parsed: WamiAction = serde_json::from_str(&json).unwrap();
961        assert_eq!(parsed, WamiAction::DbQuery);
962    }
963
964    #[test]
965    fn serde_wildcard_roundtrip() {
966        let all = WamiAction::All;
967        assert_eq!(serde_json::to_string(&all).unwrap(), r#""*""#);
968
969        let svc = WamiAction::ServiceAll(WamiServicePrefix::Iam);
970        assert_eq!(serde_json::to_string(&svc).unwrap(), r#""iam:*""#);
971
972        let parsed: WamiAction = serde_json::from_str(r#""iam:*""#).unwrap();
973        assert_eq!(parsed, WamiAction::ServiceAll(WamiServicePrefix::Iam));
974    }
975
976    #[test]
977    fn from_str_roundtrip_all_actions() {
978        // Every action should survive a roundtrip: Display → FromStr
979        let actions = vec![
980            WamiAction::All,
981            WamiAction::ServiceAll(WamiServicePrefix::Db),
982            WamiAction::PlatformAdmin,
983            WamiAction::TenantCreate,
984            WamiAction::TenantRead,
985            WamiAction::IamCreateUser,
986            WamiAction::DbQuery,
987            WamiAction::ChatSend,
988            WamiAction::PersonaInvoke,
989            WamiAction::RoomCreate,
990            WamiAction::InferenceInvoke,
991            WamiAction::AnalyticsExport,
992            WamiAction::IntegrationCreate,
993            WamiAction::CognitiveRead,
994            WamiAction::GdprEraseData,
995        ];
996        for action in actions {
997            let s = action.to_string();
998            let parsed = WamiAction::from_str(&s).unwrap_or_else(|e| {
999                panic!("Failed to parse '{}': {}", s, e);
1000            });
1001            assert_eq!(parsed, action, "Roundtrip failed for {}", s);
1002        }
1003    }
1004
1005    #[test]
1006    fn from_str_errors() {
1007        assert!(WamiAction::from_str("invalid").is_err());
1008        assert!(WamiAction::from_str("unknown:Action").is_err());
1009        assert!(WamiAction::from_str("db:NonExistent").is_err());
1010        assert!(WamiAction::from_str("").is_err());
1011    }
1012
1013    #[test]
1014    fn wildcard_matching() {
1015        let all = WamiAction::All;
1016        let iam_all = WamiAction::ServiceAll(WamiServicePrefix::Iam);
1017        let create_user = WamiAction::IamCreateUser;
1018        let db_query = WamiAction::DbQuery;
1019
1020        // * matches everything
1021        assert!(all.matches(&create_user));
1022        assert!(all.matches(&db_query));
1023        assert!(all.matches(&iam_all));
1024
1025        // iam:* matches iam:CreateUser but not db:Query
1026        assert!(iam_all.matches(&create_user));
1027        assert!(!iam_all.matches(&db_query));
1028
1029        // Exact match
1030        assert!(create_user.matches(&create_user));
1031        assert!(!create_user.matches(&db_query));
1032    }
1033
1034    #[test]
1035    fn matches_str_backward_compat() {
1036        assert!(WamiAction::matches_str("*", "db:Query"));
1037        assert!(WamiAction::matches_str("db:*", "db:Query"));
1038        assert!(WamiAction::matches_str("db:Query", "db:Query"));
1039        assert!(!WamiAction::matches_str("db:*", "iam:CreateUser"));
1040        assert!(!WamiAction::matches_str("db:Query", "db:Write"));
1041    }
1042
1043    #[test]
1044    fn prefix_extraction() {
1045        assert_eq!(WamiAction::All.prefix(), None);
1046        assert_eq!(
1047            WamiAction::ServiceAll(WamiServicePrefix::Db).prefix(),
1048            Some(WamiServicePrefix::Db)
1049        );
1050        assert_eq!(WamiAction::DbQuery.prefix(), Some(WamiServicePrefix::Db));
1051        assert_eq!(
1052            WamiAction::IamCreateUser.prefix(),
1053            Some(WamiServicePrefix::Iam)
1054        );
1055    }
1056
1057    #[test]
1058    fn registry() {
1059        let all = ActionRegistry::list_actions();
1060        // We have ~74 concrete actions (excluding wildcards)
1061        assert!(
1062            all.len() >= 70,
1063            "Expected at least 70 actions, got {}",
1064            all.len()
1065        );
1066
1067        let db_actions = ActionRegistry::list_by_prefix(WamiServicePrefix::Db);
1068        assert_eq!(db_actions.len(), 9);
1069        assert!(db_actions.iter().all(|a| a.category == "db"));
1070
1071        let iam_actions = ActionRegistry::list_by_prefix(WamiServicePrefix::Iam);
1072        assert_eq!(iam_actions.len(), 19);
1073    }
1074
1075    // -----------------------------------------------------------------------
1076    // Exhaustive list of every concrete action variant (no wildcards)
1077    // -----------------------------------------------------------------------
1078
1079    fn all_concrete_actions() -> Vec<WamiAction> {
1080        vec![
1081            // Platform
1082            WamiAction::PlatformAdmin,
1083            WamiAction::PlatformViewSettings,
1084            WamiAction::PlatformUpdateSettings,
1085            WamiAction::PlatformViewAuditLog,
1086            WamiAction::TenantCreate,
1087            WamiAction::TenantConfigure,
1088            WamiAction::TenantManageMembers,
1089            WamiAction::TenantSuspend,
1090            WamiAction::TenantResume,
1091            WamiAction::TenantList,
1092            // Tenant
1093            WamiAction::TenantRead,
1094            WamiAction::TenantUpdate,
1095            WamiAction::TenantDelete,
1096            WamiAction::TenantCreateSubTenant,
1097            WamiAction::TenantManageUsers,
1098            WamiAction::TenantManageRoles,
1099            WamiAction::TenantManagePolicies,
1100            // IAM
1101            WamiAction::IamCreateUser,
1102            WamiAction::IamDeleteUser,
1103            WamiAction::IamReadUser,
1104            WamiAction::IamUpdateUser,
1105            WamiAction::IamListUsers,
1106            WamiAction::IamCreateGroup,
1107            WamiAction::IamDeleteGroup,
1108            WamiAction::IamManageGroupMembers,
1109            WamiAction::IamCreateRole,
1110            WamiAction::IamDeleteRole,
1111            WamiAction::IamReadRole,
1112            WamiAction::IamAssumeRole,
1113            WamiAction::IamCreatePolicy,
1114            WamiAction::IamDeletePolicy,
1115            WamiAction::IamReadPolicy,
1116            WamiAction::IamAttachPolicy,
1117            WamiAction::IamDetachPolicy,
1118            WamiAction::IamSetBoundary,
1119            WamiAction::IamManageCredentials,
1120            // DB
1121            WamiAction::DbQuery,
1122            WamiAction::DbWrite,
1123            WamiAction::DbDelete,
1124            WamiAction::DbCreate,
1125            WamiAction::DbDrop,
1126            WamiAction::DbList,
1127            WamiAction::DbConfigureAccess,
1128            WamiAction::DbImport,
1129            WamiAction::DbExport,
1130            // Chat
1131            WamiAction::ChatSend,
1132            WamiAction::ChatReadHistory,
1133            WamiAction::ChatDeleteConversation,
1134            WamiAction::ChatStream,
1135            // Persona
1136            WamiAction::PersonaCreate,
1137            WamiAction::PersonaDelete,
1138            WamiAction::PersonaRead,
1139            WamiAction::PersonaUpdate,
1140            WamiAction::PersonaList,
1141            WamiAction::PersonaInvoke,
1142            // Room
1143            WamiAction::RoomCreate,
1144            WamiAction::RoomDelete,
1145            WamiAction::RoomJoin,
1146            WamiAction::RoomRead,
1147            WamiAction::RoomSend,
1148            WamiAction::RoomManage,
1149            // Inference
1150            WamiAction::InferenceListModels,
1151            WamiAction::InferenceInvoke,
1152            WamiAction::InferenceConfigureRouter,
1153            WamiAction::InferenceViewUsage,
1154            // Analytics
1155            WamiAction::AnalyticsViewUsage,
1156            WamiAction::AnalyticsViewConversations,
1157            WamiAction::AnalyticsExport,
1158            WamiAction::AnalyticsViewActivity,
1159            // Integration
1160            WamiAction::IntegrationCreate,
1161            WamiAction::IntegrationDelete,
1162            WamiAction::IntegrationConfigure,
1163            WamiAction::IntegrationList,
1164            WamiAction::IntegrationReceive,
1165            WamiAction::IntegrationSend,
1166            // Cognitive
1167            WamiAction::CognitiveRead,
1168            WamiAction::CognitiveWrite,
1169            WamiAction::CognitiveReset,
1170            // GDPR
1171            WamiAction::GdprGrantConsent,
1172            WamiAction::GdprRevokeConsent,
1173            WamiAction::GdprExportData,
1174            WamiAction::GdprEraseData,
1175            WamiAction::GdprViewAudit,
1176        ]
1177    }
1178
1179    // -----------------------------------------------------------------------
1180    // Round-trip: as_str -> from_str for EVERY variant
1181    // -----------------------------------------------------------------------
1182
1183    #[test]
1184    fn roundtrip_every_concrete_action() {
1185        for action in all_concrete_actions() {
1186            let s = action.as_str();
1187            let parsed = WamiAction::from_str(s)
1188                .unwrap_or_else(|e| panic!("from_str({:?}) failed: {}", s, e));
1189            assert_eq!(parsed, action, "roundtrip failed for {:?}", s);
1190        }
1191    }
1192
1193    #[test]
1194    fn roundtrip_all_service_wildcards() {
1195        for prefix in WamiServicePrefix::all() {
1196            let action = WamiAction::ServiceAll(*prefix);
1197            let s = action.as_str();
1198            assert!(s.ends_with(":*"), "ServiceAll as_str should end with :*");
1199            let parsed = WamiAction::from_str(s).unwrap();
1200            assert_eq!(parsed, action);
1201        }
1202    }
1203
1204    #[test]
1205    fn roundtrip_global_wildcard() {
1206        assert_eq!(WamiAction::from_str("*").unwrap(), WamiAction::All);
1207        assert_eq!(WamiAction::All.as_str(), "*");
1208    }
1209
1210    // -----------------------------------------------------------------------
1211    // Display -> FromStr for every variant (exercises Display impl)
1212    // -----------------------------------------------------------------------
1213
1214    #[test]
1215    fn display_fromstr_every_action() {
1216        for action in all_concrete_actions() {
1217            let display = action.to_string();
1218            let parsed: WamiAction = display.parse().unwrap();
1219            assert_eq!(parsed, action);
1220        }
1221        // Wildcards too
1222        let all_display = WamiAction::All.to_string();
1223        assert_eq!(all_display, "*");
1224        assert_eq!(all_display.parse::<WamiAction>().unwrap(), WamiAction::All);
1225
1226        for prefix in WamiServicePrefix::all() {
1227            let svc = WamiAction::ServiceAll(*prefix);
1228            let display = svc.to_string();
1229            assert_eq!(display.parse::<WamiAction>().unwrap(), svc);
1230        }
1231    }
1232
1233    // -----------------------------------------------------------------------
1234    // prefix() for EVERY variant
1235    // -----------------------------------------------------------------------
1236
1237    #[test]
1238    fn prefix_every_concrete_action() {
1239        let expected: Vec<(WamiAction, WamiServicePrefix)> = vec![
1240            (WamiAction::PlatformAdmin, WamiServicePrefix::Platform),
1241            (
1242                WamiAction::PlatformViewSettings,
1243                WamiServicePrefix::Platform,
1244            ),
1245            (
1246                WamiAction::PlatformUpdateSettings,
1247                WamiServicePrefix::Platform,
1248            ),
1249            (
1250                WamiAction::PlatformViewAuditLog,
1251                WamiServicePrefix::Platform,
1252            ),
1253            (WamiAction::TenantCreate, WamiServicePrefix::Tenant),
1254            (WamiAction::TenantConfigure, WamiServicePrefix::Tenant),
1255            (WamiAction::TenantManageMembers, WamiServicePrefix::Tenant),
1256            (WamiAction::TenantSuspend, WamiServicePrefix::Tenant),
1257            (WamiAction::TenantResume, WamiServicePrefix::Tenant),
1258            (WamiAction::TenantList, WamiServicePrefix::Tenant),
1259            (WamiAction::TenantRead, WamiServicePrefix::Tenant),
1260            (WamiAction::TenantUpdate, WamiServicePrefix::Tenant),
1261            (WamiAction::TenantDelete, WamiServicePrefix::Tenant),
1262            (WamiAction::TenantCreateSubTenant, WamiServicePrefix::Tenant),
1263            (WamiAction::TenantManageUsers, WamiServicePrefix::Tenant),
1264            (WamiAction::TenantManageRoles, WamiServicePrefix::Tenant),
1265            (WamiAction::TenantManagePolicies, WamiServicePrefix::Tenant),
1266            (WamiAction::IamCreateUser, WamiServicePrefix::Iam),
1267            (WamiAction::IamDeleteUser, WamiServicePrefix::Iam),
1268            (WamiAction::IamReadUser, WamiServicePrefix::Iam),
1269            (WamiAction::IamUpdateUser, WamiServicePrefix::Iam),
1270            (WamiAction::IamListUsers, WamiServicePrefix::Iam),
1271            (WamiAction::IamCreateGroup, WamiServicePrefix::Iam),
1272            (WamiAction::IamDeleteGroup, WamiServicePrefix::Iam),
1273            (WamiAction::IamManageGroupMembers, WamiServicePrefix::Iam),
1274            (WamiAction::IamCreateRole, WamiServicePrefix::Iam),
1275            (WamiAction::IamDeleteRole, WamiServicePrefix::Iam),
1276            (WamiAction::IamReadRole, WamiServicePrefix::Iam),
1277            (WamiAction::IamAssumeRole, WamiServicePrefix::Iam),
1278            (WamiAction::IamCreatePolicy, WamiServicePrefix::Iam),
1279            (WamiAction::IamDeletePolicy, WamiServicePrefix::Iam),
1280            (WamiAction::IamReadPolicy, WamiServicePrefix::Iam),
1281            (WamiAction::IamAttachPolicy, WamiServicePrefix::Iam),
1282            (WamiAction::IamDetachPolicy, WamiServicePrefix::Iam),
1283            (WamiAction::IamSetBoundary, WamiServicePrefix::Iam),
1284            (WamiAction::IamManageCredentials, WamiServicePrefix::Iam),
1285            (WamiAction::DbQuery, WamiServicePrefix::Db),
1286            (WamiAction::DbWrite, WamiServicePrefix::Db),
1287            (WamiAction::DbDelete, WamiServicePrefix::Db),
1288            (WamiAction::DbCreate, WamiServicePrefix::Db),
1289            (WamiAction::DbDrop, WamiServicePrefix::Db),
1290            (WamiAction::DbList, WamiServicePrefix::Db),
1291            (WamiAction::DbConfigureAccess, WamiServicePrefix::Db),
1292            (WamiAction::DbImport, WamiServicePrefix::Db),
1293            (WamiAction::DbExport, WamiServicePrefix::Db),
1294            (WamiAction::ChatSend, WamiServicePrefix::Chat),
1295            (WamiAction::ChatReadHistory, WamiServicePrefix::Chat),
1296            (WamiAction::ChatDeleteConversation, WamiServicePrefix::Chat),
1297            (WamiAction::ChatStream, WamiServicePrefix::Chat),
1298            (WamiAction::PersonaCreate, WamiServicePrefix::Persona),
1299            (WamiAction::PersonaDelete, WamiServicePrefix::Persona),
1300            (WamiAction::PersonaRead, WamiServicePrefix::Persona),
1301            (WamiAction::PersonaUpdate, WamiServicePrefix::Persona),
1302            (WamiAction::PersonaList, WamiServicePrefix::Persona),
1303            (WamiAction::PersonaInvoke, WamiServicePrefix::Persona),
1304            (WamiAction::RoomCreate, WamiServicePrefix::Room),
1305            (WamiAction::RoomDelete, WamiServicePrefix::Room),
1306            (WamiAction::RoomJoin, WamiServicePrefix::Room),
1307            (WamiAction::RoomRead, WamiServicePrefix::Room),
1308            (WamiAction::RoomSend, WamiServicePrefix::Room),
1309            (WamiAction::RoomManage, WamiServicePrefix::Room),
1310            (
1311                WamiAction::InferenceListModels,
1312                WamiServicePrefix::Inference,
1313            ),
1314            (WamiAction::InferenceInvoke, WamiServicePrefix::Inference),
1315            (
1316                WamiAction::InferenceConfigureRouter,
1317                WamiServicePrefix::Inference,
1318            ),
1319            (WamiAction::InferenceViewUsage, WamiServicePrefix::Inference),
1320            (WamiAction::AnalyticsViewUsage, WamiServicePrefix::Analytics),
1321            (
1322                WamiAction::AnalyticsViewConversations,
1323                WamiServicePrefix::Analytics,
1324            ),
1325            (WamiAction::AnalyticsExport, WamiServicePrefix::Analytics),
1326            (
1327                WamiAction::AnalyticsViewActivity,
1328                WamiServicePrefix::Analytics,
1329            ),
1330            (
1331                WamiAction::IntegrationCreate,
1332                WamiServicePrefix::Integration,
1333            ),
1334            (
1335                WamiAction::IntegrationDelete,
1336                WamiServicePrefix::Integration,
1337            ),
1338            (
1339                WamiAction::IntegrationConfigure,
1340                WamiServicePrefix::Integration,
1341            ),
1342            (WamiAction::IntegrationList, WamiServicePrefix::Integration),
1343            (
1344                WamiAction::IntegrationReceive,
1345                WamiServicePrefix::Integration,
1346            ),
1347            (WamiAction::IntegrationSend, WamiServicePrefix::Integration),
1348            (WamiAction::CognitiveRead, WamiServicePrefix::Cognitive),
1349            (WamiAction::CognitiveWrite, WamiServicePrefix::Cognitive),
1350            (WamiAction::CognitiveReset, WamiServicePrefix::Cognitive),
1351            (WamiAction::GdprGrantConsent, WamiServicePrefix::Gdpr),
1352            (WamiAction::GdprRevokeConsent, WamiServicePrefix::Gdpr),
1353            (WamiAction::GdprExportData, WamiServicePrefix::Gdpr),
1354            (WamiAction::GdprEraseData, WamiServicePrefix::Gdpr),
1355            (WamiAction::GdprViewAudit, WamiServicePrefix::Gdpr),
1356        ];
1357
1358        for (action, expected_prefix) in &expected {
1359            assert_eq!(
1360                action.prefix(),
1361                Some(*expected_prefix),
1362                "prefix() wrong for {:?}",
1363                action
1364            );
1365        }
1366    }
1367
1368    #[test]
1369    fn prefix_wildcards() {
1370        assert_eq!(WamiAction::All.prefix(), None);
1371        for prefix in WamiServicePrefix::all() {
1372            assert_eq!(WamiAction::ServiceAll(*prefix).prefix(), Some(*prefix),);
1373        }
1374    }
1375
1376    // -----------------------------------------------------------------------
1377    // as_str() spot-checks for every service group (exercises all match arms)
1378    // -----------------------------------------------------------------------
1379
1380    #[test]
1381    fn as_str_platform() {
1382        assert_eq!(WamiAction::PlatformAdmin.as_str(), "platform:Admin");
1383        assert_eq!(
1384            WamiAction::PlatformViewSettings.as_str(),
1385            "platform:ViewSettings"
1386        );
1387        assert_eq!(
1388            WamiAction::PlatformUpdateSettings.as_str(),
1389            "platform:UpdateSettings"
1390        );
1391        assert_eq!(
1392            WamiAction::PlatformViewAuditLog.as_str(),
1393            "platform:ViewAuditLog"
1394        );
1395    }
1396
1397    #[test]
1398    fn as_str_tenant_lifecycle() {
1399        assert_eq!(WamiAction::TenantCreate.as_str(), "tenant:Create");
1400        assert_eq!(WamiAction::TenantConfigure.as_str(), "tenant:Configure");
1401        assert_eq!(
1402            WamiAction::TenantManageMembers.as_str(),
1403            "tenant:ManageMembers"
1404        );
1405        assert_eq!(WamiAction::TenantSuspend.as_str(), "tenant:Suspend");
1406        assert_eq!(WamiAction::TenantResume.as_str(), "tenant:Resume");
1407        assert_eq!(WamiAction::TenantList.as_str(), "tenant:List");
1408    }
1409
1410    #[test]
1411    fn as_str_tenant() {
1412        assert_eq!(WamiAction::TenantRead.as_str(), "tenant:Read");
1413        assert_eq!(WamiAction::TenantUpdate.as_str(), "tenant:Update");
1414        assert_eq!(WamiAction::TenantDelete.as_str(), "tenant:Delete");
1415        assert_eq!(
1416            WamiAction::TenantCreateSubTenant.as_str(),
1417            "tenant:CreateSubTenant"
1418        );
1419        assert_eq!(WamiAction::TenantManageUsers.as_str(), "tenant:ManageUsers");
1420        assert_eq!(WamiAction::TenantManageRoles.as_str(), "tenant:ManageRoles");
1421        assert_eq!(
1422            WamiAction::TenantManagePolicies.as_str(),
1423            "tenant:ManagePolicies"
1424        );
1425    }
1426
1427    #[test]
1428    fn as_str_iam() {
1429        assert_eq!(WamiAction::IamCreateUser.as_str(), "iam:CreateUser");
1430        assert_eq!(WamiAction::IamDeleteUser.as_str(), "iam:DeleteUser");
1431        assert_eq!(WamiAction::IamReadUser.as_str(), "iam:ReadUser");
1432        assert_eq!(WamiAction::IamUpdateUser.as_str(), "iam:UpdateUser");
1433        assert_eq!(WamiAction::IamListUsers.as_str(), "iam:ListUsers");
1434        assert_eq!(WamiAction::IamCreateGroup.as_str(), "iam:CreateGroup");
1435        assert_eq!(WamiAction::IamDeleteGroup.as_str(), "iam:DeleteGroup");
1436        assert_eq!(
1437            WamiAction::IamManageGroupMembers.as_str(),
1438            "iam:ManageGroupMembers"
1439        );
1440        assert_eq!(WamiAction::IamCreateRole.as_str(), "iam:CreateRole");
1441        assert_eq!(WamiAction::IamDeleteRole.as_str(), "iam:DeleteRole");
1442        assert_eq!(WamiAction::IamReadRole.as_str(), "iam:ReadRole");
1443        assert_eq!(WamiAction::IamAssumeRole.as_str(), "iam:AssumeRole");
1444        assert_eq!(WamiAction::IamCreatePolicy.as_str(), "iam:CreatePolicy");
1445        assert_eq!(WamiAction::IamDeletePolicy.as_str(), "iam:DeletePolicy");
1446        assert_eq!(WamiAction::IamReadPolicy.as_str(), "iam:ReadPolicy");
1447        assert_eq!(WamiAction::IamAttachPolicy.as_str(), "iam:AttachPolicy");
1448        assert_eq!(WamiAction::IamDetachPolicy.as_str(), "iam:DetachPolicy");
1449        assert_eq!(WamiAction::IamSetBoundary.as_str(), "iam:SetBoundary");
1450        assert_eq!(
1451            WamiAction::IamManageCredentials.as_str(),
1452            "iam:ManageCredentials"
1453        );
1454    }
1455
1456    #[test]
1457    fn as_str_db() {
1458        assert_eq!(WamiAction::DbQuery.as_str(), "db:Query");
1459        assert_eq!(WamiAction::DbWrite.as_str(), "db:Write");
1460        assert_eq!(WamiAction::DbDelete.as_str(), "db:Delete");
1461        assert_eq!(WamiAction::DbCreate.as_str(), "db:Create");
1462        assert_eq!(WamiAction::DbDrop.as_str(), "db:Drop");
1463        assert_eq!(WamiAction::DbList.as_str(), "db:List");
1464        assert_eq!(WamiAction::DbConfigureAccess.as_str(), "db:ConfigureAccess");
1465        assert_eq!(WamiAction::DbImport.as_str(), "db:Import");
1466        assert_eq!(WamiAction::DbExport.as_str(), "db:Export");
1467    }
1468
1469    #[test]
1470    fn as_str_chat() {
1471        assert_eq!(WamiAction::ChatSend.as_str(), "chat:Send");
1472        assert_eq!(WamiAction::ChatReadHistory.as_str(), "chat:ReadHistory");
1473        assert_eq!(
1474            WamiAction::ChatDeleteConversation.as_str(),
1475            "chat:DeleteConversation"
1476        );
1477        assert_eq!(WamiAction::ChatStream.as_str(), "chat:Stream");
1478    }
1479
1480    #[test]
1481    fn as_str_persona() {
1482        assert_eq!(WamiAction::PersonaCreate.as_str(), "persona:Create");
1483        assert_eq!(WamiAction::PersonaDelete.as_str(), "persona:Delete");
1484        assert_eq!(WamiAction::PersonaRead.as_str(), "persona:Read");
1485        assert_eq!(WamiAction::PersonaUpdate.as_str(), "persona:Update");
1486        assert_eq!(WamiAction::PersonaList.as_str(), "persona:List");
1487        assert_eq!(WamiAction::PersonaInvoke.as_str(), "persona:Invoke");
1488    }
1489
1490    #[test]
1491    fn as_str_room() {
1492        assert_eq!(WamiAction::RoomCreate.as_str(), "room:Create");
1493        assert_eq!(WamiAction::RoomDelete.as_str(), "room:Delete");
1494        assert_eq!(WamiAction::RoomJoin.as_str(), "room:Join");
1495        assert_eq!(WamiAction::RoomRead.as_str(), "room:Read");
1496        assert_eq!(WamiAction::RoomSend.as_str(), "room:Send");
1497        assert_eq!(WamiAction::RoomManage.as_str(), "room:Manage");
1498    }
1499
1500    #[test]
1501    fn as_str_inference() {
1502        assert_eq!(
1503            WamiAction::InferenceListModels.as_str(),
1504            "inference:ListModels"
1505        );
1506        assert_eq!(WamiAction::InferenceInvoke.as_str(), "inference:Invoke");
1507        assert_eq!(
1508            WamiAction::InferenceConfigureRouter.as_str(),
1509            "inference:ConfigureRouter"
1510        );
1511        assert_eq!(
1512            WamiAction::InferenceViewUsage.as_str(),
1513            "inference:ViewUsage"
1514        );
1515    }
1516
1517    #[test]
1518    fn as_str_analytics() {
1519        assert_eq!(
1520            WamiAction::AnalyticsViewUsage.as_str(),
1521            "analytics:ViewUsage"
1522        );
1523        assert_eq!(
1524            WamiAction::AnalyticsViewConversations.as_str(),
1525            "analytics:ViewConversations"
1526        );
1527        assert_eq!(WamiAction::AnalyticsExport.as_str(), "analytics:Export");
1528        assert_eq!(
1529            WamiAction::AnalyticsViewActivity.as_str(),
1530            "analytics:ViewActivity"
1531        );
1532    }
1533
1534    #[test]
1535    fn as_str_integration() {
1536        assert_eq!(WamiAction::IntegrationCreate.as_str(), "integration:Create");
1537        assert_eq!(WamiAction::IntegrationDelete.as_str(), "integration:Delete");
1538        assert_eq!(
1539            WamiAction::IntegrationConfigure.as_str(),
1540            "integration:Configure"
1541        );
1542        assert_eq!(WamiAction::IntegrationList.as_str(), "integration:List");
1543        assert_eq!(
1544            WamiAction::IntegrationReceive.as_str(),
1545            "integration:Receive"
1546        );
1547        assert_eq!(WamiAction::IntegrationSend.as_str(), "integration:Send");
1548    }
1549
1550    #[test]
1551    fn as_str_cognitive() {
1552        assert_eq!(WamiAction::CognitiveRead.as_str(), "cognitive:Read");
1553        assert_eq!(WamiAction::CognitiveWrite.as_str(), "cognitive:Write");
1554        assert_eq!(WamiAction::CognitiveReset.as_str(), "cognitive:Reset");
1555    }
1556
1557    #[test]
1558    fn as_str_gdpr() {
1559        assert_eq!(WamiAction::GdprGrantConsent.as_str(), "gdpr:GrantConsent");
1560        assert_eq!(WamiAction::GdprRevokeConsent.as_str(), "gdpr:RevokeConsent");
1561        assert_eq!(WamiAction::GdprExportData.as_str(), "gdpr:ExportData");
1562        assert_eq!(WamiAction::GdprEraseData.as_str(), "gdpr:EraseData");
1563        assert_eq!(WamiAction::GdprViewAudit.as_str(), "gdpr:ViewAudit");
1564    }
1565
1566    // -----------------------------------------------------------------------
1567    // ServiceAll as_str for every prefix
1568    // -----------------------------------------------------------------------
1569
1570    #[test]
1571    fn as_str_service_all_every_prefix() {
1572        assert_eq!(
1573            WamiAction::ServiceAll(WamiServicePrefix::Platform).as_str(),
1574            "platform:*"
1575        );
1576        assert_eq!(
1577            WamiAction::ServiceAll(WamiServicePrefix::Tenant).as_str(),
1578            "tenant:*"
1579        );
1580        assert_eq!(
1581            WamiAction::ServiceAll(WamiServicePrefix::Tenant).as_str(),
1582            "tenant:*"
1583        );
1584        assert_eq!(
1585            WamiAction::ServiceAll(WamiServicePrefix::Iam).as_str(),
1586            "iam:*"
1587        );
1588        assert_eq!(
1589            WamiAction::ServiceAll(WamiServicePrefix::Db).as_str(),
1590            "db:*"
1591        );
1592        assert_eq!(
1593            WamiAction::ServiceAll(WamiServicePrefix::Chat).as_str(),
1594            "chat:*"
1595        );
1596        assert_eq!(
1597            WamiAction::ServiceAll(WamiServicePrefix::Persona).as_str(),
1598            "persona:*"
1599        );
1600        assert_eq!(
1601            WamiAction::ServiceAll(WamiServicePrefix::Room).as_str(),
1602            "room:*"
1603        );
1604        assert_eq!(
1605            WamiAction::ServiceAll(WamiServicePrefix::Inference).as_str(),
1606            "inference:*"
1607        );
1608        assert_eq!(
1609            WamiAction::ServiceAll(WamiServicePrefix::Analytics).as_str(),
1610            "analytics:*"
1611        );
1612        assert_eq!(
1613            WamiAction::ServiceAll(WamiServicePrefix::Integration).as_str(),
1614            "integration:*"
1615        );
1616        assert_eq!(
1617            WamiAction::ServiceAll(WamiServicePrefix::Cognitive).as_str(),
1618            "cognitive:*"
1619        );
1620        assert_eq!(
1621            WamiAction::ServiceAll(WamiServicePrefix::Gdpr).as_str(),
1622            "gdpr:*"
1623        );
1624    }
1625
1626    // -----------------------------------------------------------------------
1627    // Wildcard matching — exhaustive cross-service checks
1628    // -----------------------------------------------------------------------
1629
1630    #[test]
1631    fn matches_all_matches_everything() {
1632        let all = WamiAction::All;
1633        for action in all_concrete_actions() {
1634            assert!(all.matches(&action), "All should match {:?}", action);
1635        }
1636        // Also matches wildcards themselves
1637        for prefix in WamiServicePrefix::all() {
1638            assert!(all.matches(&WamiAction::ServiceAll(*prefix)));
1639        }
1640        assert!(all.matches(&WamiAction::All));
1641    }
1642
1643    #[test]
1644    fn service_all_matches_only_own_prefix() {
1645        // For every prefix, ServiceAll should match all actions in that prefix
1646        // and NOT match actions in other prefixes
1647        let actions = all_concrete_actions();
1648        for prefix in WamiServicePrefix::all() {
1649            let svc_all = WamiAction::ServiceAll(*prefix);
1650            for action in &actions {
1651                if action.prefix() == Some(*prefix) {
1652                    assert!(
1653                        svc_all.matches(action),
1654                        "{:?} should match {:?}",
1655                        svc_all,
1656                        action
1657                    );
1658                } else {
1659                    assert!(
1660                        !svc_all.matches(action),
1661                        "{:?} should NOT match {:?}",
1662                        svc_all,
1663                        action
1664                    );
1665                }
1666            }
1667        }
1668    }
1669
1670    #[test]
1671    fn exact_match_only_matches_self() {
1672        let actions = all_concrete_actions();
1673        for a in &actions {
1674            assert!(a.matches(a), "{:?} should match itself", a);
1675            for b in &actions {
1676                if a != b {
1677                    assert!(!a.matches(b), "{:?} should not match {:?}", a, b);
1678                }
1679            }
1680        }
1681    }
1682
1683    #[test]
1684    fn concrete_does_not_match_service_all() {
1685        // A concrete action should not match a ServiceAll pattern
1686        let iam_create = WamiAction::IamCreateUser;
1687        let iam_all = WamiAction::ServiceAll(WamiServicePrefix::Iam);
1688        assert!(!iam_create.matches(&iam_all));
1689    }
1690
1691    // -----------------------------------------------------------------------
1692    // matches_str — backward compat string matching
1693    // -----------------------------------------------------------------------
1694
1695    #[test]
1696    fn matches_str_star_matches_everything() {
1697        for action in all_concrete_actions() {
1698            assert!(WamiAction::matches_str("*", action.as_str()));
1699        }
1700    }
1701
1702    #[test]
1703    fn matches_str_service_wildcard_every_prefix() {
1704        let prefixes_and_actions: Vec<(&str, &str, &str)> = vec![
1705            ("platform:*", "platform:Admin", "tenant:Create"),
1706            ("platform:*", "platform:Admin", "tenant:Read"),
1707            ("tenant:*", "tenant:Read", "iam:CreateUser"),
1708            ("iam:*", "iam:CreateUser", "db:Query"),
1709            ("db:*", "db:Query", "chat:Send"),
1710            ("chat:*", "chat:Send", "persona:Create"),
1711            ("persona:*", "persona:Create", "room:Create"),
1712            ("room:*", "room:Create", "inference:Invoke"),
1713            ("inference:*", "inference:Invoke", "analytics:Export"),
1714            ("analytics:*", "analytics:Export", "integration:Create"),
1715            ("integration:*", "integration:Create", "cognitive:Read"),
1716            ("cognitive:*", "cognitive:Read", "gdpr:GrantConsent"),
1717            ("gdpr:*", "gdpr:GrantConsent", "platform:Admin"),
1718        ];
1719        for (pattern, should_match, should_not_match) in prefixes_and_actions {
1720            assert!(
1721                WamiAction::matches_str(pattern, should_match),
1722                "{} should match {}",
1723                pattern,
1724                should_match
1725            );
1726            assert!(
1727                !WamiAction::matches_str(pattern, should_not_match),
1728                "{} should NOT match {}",
1729                pattern,
1730                should_not_match
1731            );
1732        }
1733    }
1734
1735    #[test]
1736    fn matches_str_exact() {
1737        assert!(WamiAction::matches_str("iam:CreateUser", "iam:CreateUser"));
1738        assert!(!WamiAction::matches_str("iam:CreateUser", "iam:DeleteUser"));
1739    }
1740
1741    #[test]
1742    fn matches_str_no_colon_in_requested() {
1743        // If the requested string has no colon, prefix wildcard should not match
1744        assert!(!WamiAction::matches_str("iam:*", "noColonHere"));
1745    }
1746
1747    #[test]
1748    fn matches_str_non_matching_patterns() {
1749        assert!(!WamiAction::matches_str("db:Query", "db:Write"));
1750        assert!(!WamiAction::matches_str("iam:*", "db:Query"));
1751        assert!(!WamiAction::matches_str("platform:Admin", "*"));
1752    }
1753
1754    // -----------------------------------------------------------------------
1755    // Edge cases — parse errors
1756    // -----------------------------------------------------------------------
1757
1758    #[test]
1759    fn from_str_empty_string() {
1760        let err = WamiAction::from_str("").unwrap_err();
1761        assert!(matches!(err, ActionParseError::InvalidFormat(_)));
1762    }
1763
1764    #[test]
1765    fn from_str_no_colon() {
1766        let err = WamiAction::from_str("noColon").unwrap_err();
1767        assert!(matches!(err, ActionParseError::InvalidFormat(_)));
1768    }
1769
1770    #[test]
1771    fn from_str_unknown_prefix() {
1772        let err = WamiAction::from_str("fake:*").unwrap_err();
1773        assert!(matches!(err, ActionParseError::UnknownPrefix(_)));
1774    }
1775
1776    #[test]
1777    fn from_str_unknown_operation() {
1778        let err = WamiAction::from_str("iam:FakeOp").unwrap_err();
1779        assert!(matches!(err, ActionParseError::Unknown(_)));
1780    }
1781
1782    #[test]
1783    fn from_str_case_sensitive() {
1784        // Should be case-sensitive: "IAM:CreateUser" is not valid
1785        assert!(WamiAction::from_str("IAM:CreateUser").is_err());
1786        assert!(WamiAction::from_str("iam:createuser").is_err());
1787        assert!(WamiAction::from_str("Iam:CreateUser").is_err());
1788        assert!(WamiAction::from_str("iam:createUser").is_err());
1789    }
1790
1791    #[test]
1792    fn from_str_valid_prefix_wrong_operation() {
1793        assert!(WamiAction::from_str("db:NotAnAction").is_err());
1794        assert!(WamiAction::from_str("chat:NotAnAction").is_err());
1795        assert!(WamiAction::from_str("room:NotAnAction").is_err());
1796    }
1797
1798    // -----------------------------------------------------------------------
1799    // WamiServicePrefix tests
1800    // -----------------------------------------------------------------------
1801
1802    #[test]
1803    fn service_prefix_as_str_all() {
1804        assert_eq!(WamiServicePrefix::Platform.as_str(), "platform");
1805        assert_eq!(WamiServicePrefix::Tenant.as_str(), "tenant");
1806        assert_eq!(WamiServicePrefix::Iam.as_str(), "iam");
1807        assert_eq!(WamiServicePrefix::Db.as_str(), "db");
1808        assert_eq!(WamiServicePrefix::Chat.as_str(), "chat");
1809        assert_eq!(WamiServicePrefix::Persona.as_str(), "persona");
1810        assert_eq!(WamiServicePrefix::Room.as_str(), "room");
1811        assert_eq!(WamiServicePrefix::Inference.as_str(), "inference");
1812        assert_eq!(WamiServicePrefix::Analytics.as_str(), "analytics");
1813        assert_eq!(WamiServicePrefix::Integration.as_str(), "integration");
1814        assert_eq!(WamiServicePrefix::Cognitive.as_str(), "cognitive");
1815        assert_eq!(WamiServicePrefix::Gdpr.as_str(), "gdpr");
1816    }
1817
1818    #[test]
1819    fn service_prefix_from_str_all() {
1820        for prefix in WamiServicePrefix::all() {
1821            let s = prefix.as_str();
1822            let parsed = WamiServicePrefix::from_str(s).unwrap();
1823            assert_eq!(parsed, *prefix);
1824        }
1825    }
1826
1827    #[test]
1828    fn service_prefix_from_str_error() {
1829        let err = WamiServicePrefix::from_str("nonexistent").unwrap_err();
1830        assert!(matches!(err, ActionParseError::UnknownPrefix(_)));
1831    }
1832
1833    #[test]
1834    fn service_prefix_display() {
1835        for prefix in WamiServicePrefix::all() {
1836            assert_eq!(prefix.to_string(), prefix.as_str());
1837        }
1838    }
1839
1840    #[test]
1841    fn service_prefix_all_returns_all_12() {
1842        assert_eq!(WamiServicePrefix::all().len(), 12);
1843    }
1844
1845    // -----------------------------------------------------------------------
1846    // Serde — additional edge cases
1847    // -----------------------------------------------------------------------
1848
1849    #[test]
1850    fn serde_roundtrip_every_action() {
1851        for action in all_concrete_actions() {
1852            let json = serde_json::to_string(&action).unwrap();
1853            let parsed: WamiAction = serde_json::from_str(&json).unwrap();
1854            assert_eq!(parsed, action);
1855        }
1856    }
1857
1858    #[test]
1859    fn serde_roundtrip_every_service_wildcard() {
1860        for prefix in WamiServicePrefix::all() {
1861            let action = WamiAction::ServiceAll(*prefix);
1862            let json = serde_json::to_string(&action).unwrap();
1863            let parsed: WamiAction = serde_json::from_str(&json).unwrap();
1864            assert_eq!(parsed, action);
1865        }
1866    }
1867
1868    #[test]
1869    fn serde_deserialize_invalid() {
1870        let result: Result<WamiAction, _> = serde_json::from_str(r#""not:valid:action""#);
1871        assert!(result.is_err());
1872
1873        let result: Result<WamiAction, _> = serde_json::from_str(r#""garbage""#);
1874        assert!(result.is_err());
1875    }
1876
1877    // -----------------------------------------------------------------------
1878    // ActionParseError Display
1879    // -----------------------------------------------------------------------
1880
1881    #[test]
1882    fn action_parse_error_display() {
1883        let e1 = ActionParseError::Unknown("foo:bar".into());
1884        assert!(e1.to_string().contains("foo:bar"));
1885
1886        let e2 = ActionParseError::UnknownPrefix("xyz".into());
1887        assert!(e2.to_string().contains("xyz"));
1888
1889        let e3 = ActionParseError::InvalidFormat("nocolon".into());
1890        assert!(e3.to_string().contains("nocolon"));
1891    }
1892
1893    // -----------------------------------------------------------------------
1894    // ActionRegistry — additional coverage
1895    // -----------------------------------------------------------------------
1896
1897    #[test]
1898    fn registry_list_by_every_prefix() {
1899        let expected_counts: Vec<(WamiServicePrefix, usize)> = vec![
1900            (WamiServicePrefix::Platform, 4),
1901            // Douze : les sept d origine, plus les cinq qui vivaient sous
1902            // `space` et ne s en distinguaient pas.
1903            (WamiServicePrefix::Tenant, 13),
1904            (WamiServicePrefix::Iam, 19),
1905            (WamiServicePrefix::Db, 9),
1906            (WamiServicePrefix::Chat, 4),
1907            (WamiServicePrefix::Persona, 6),
1908            (WamiServicePrefix::Room, 6),
1909            (WamiServicePrefix::Inference, 4),
1910            (WamiServicePrefix::Analytics, 4),
1911            (WamiServicePrefix::Integration, 6),
1912            (WamiServicePrefix::Cognitive, 3),
1913            (WamiServicePrefix::Gdpr, 5),
1914        ];
1915        for (prefix, expected) in expected_counts {
1916            let actions = ActionRegistry::list_by_prefix(prefix);
1917            assert_eq!(
1918                actions.len(),
1919                expected,
1920                "Wrong count for {:?}: got {}, expected {}",
1921                prefix,
1922                actions.len(),
1923                expected
1924            );
1925            for a in &actions {
1926                assert_eq!(a.category, prefix.as_str());
1927            }
1928        }
1929    }
1930
1931    #[test]
1932    fn registry_list_prefixes() {
1933        let prefixes = ActionRegistry::list_prefixes();
1934        assert_eq!(prefixes.len(), 12);
1935    }
1936
1937    #[test]
1938    fn registry_total_action_count() {
1939        let all = ActionRegistry::list_actions();
1940        // 4+8+7+19+9+4+6+6+4+4+6+3+5 = 85
1941        assert_eq!(all.len(), 83);
1942    }
1943}