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