Skip to main content

coil_admin/model/
shell.rs

1use super::*;
2
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub struct AdminShell {
5    accessibility: AccessibilityContract,
6    resources: Vec<AdminResourceDescriptor>,
7    widgets: Vec<AdminWidgetDescriptor>,
8    workflows: Vec<WorkflowAction>,
9    audit_log: Vec<AuditEntry>,
10}
11
12impl AdminShell {
13    pub fn new(
14        accessibility: AccessibilityContract,
15        resources: Vec<AdminResourceDescriptor>,
16        widgets: Vec<AdminWidgetDescriptor>,
17        workflows: Vec<WorkflowAction>,
18    ) -> Result<Self, AdminModelError> {
19        ensure_unique_resources(&resources)?;
20        ensure_unique_widgets(&widgets)?;
21        ensure_unique_workflows(&workflows)?;
22        Ok(Self {
23            accessibility,
24            resources,
25            widgets,
26            workflows,
27            audit_log: Vec::new(),
28        })
29    }
30
31    pub fn accessibility(&self) -> &AccessibilityContract {
32        &self.accessibility
33    }
34
35    pub fn resources(&self) -> &[AdminResourceDescriptor] {
36        &self.resources
37    }
38
39    pub fn widgets(&self) -> &[AdminWidgetDescriptor] {
40        &self.widgets
41    }
42
43    pub fn workflows(&self) -> &[WorkflowAction] {
44        &self.workflows
45    }
46
47    pub fn visible_resources(
48        &self,
49        operator: &OperatorAccessContext,
50    ) -> Vec<AdminResourceDescriptor> {
51        self.resources
52            .iter()
53            .filter(|resource| operator.allows(resource.required_capability))
54            .cloned()
55            .collect()
56    }
57
58    pub fn compose_module_resources(
59        manifests: &[ModuleManifest],
60    ) -> Result<Vec<AdminResourceDescriptor>, AdminModelError> {
61        let mut resources = Vec::new();
62        for manifest in manifests {
63            for contribution in &manifest.admin_resources {
64                resources.push(AdminResourceDescriptor::from_contribution(contribution)?);
65            }
66        }
67        ensure_unique_resources(&resources)?;
68        Ok(resources)
69    }
70
71    pub fn compose_module_workflows(
72        manifests: &[ModuleManifest],
73    ) -> Result<Vec<WorkflowAction>, AdminModelError> {
74        let mut workflows = Vec::new();
75        for manifest in manifests {
76            for definition in &manifest.bulk_operations {
77                workflows.push(WorkflowAction::from_bulk_operation(definition)?);
78            }
79        }
80        ensure_unique_workflows(&workflows)?;
81        Ok(workflows)
82    }
83
84    pub fn compose_extension_widgets(
85        registry: &ExtensionRegistry,
86    ) -> Result<Vec<AdminWidgetDescriptor>, AdminModelError> {
87        let mut widgets = Vec::new();
88
89        for handler in registry.registered_handlers() {
90            if handler.point != coil_wasm::ExtensionPointKind::AdminWidget {
91                continue;
92            }
93
94            widgets.push(AdminWidgetDescriptor::new(
95                AdminWidgetId::new(format!(
96                    "ext.{}.{}",
97                    handler.extension_id, handler.handler_id
98                ))?,
99                format!("{} widget", handler.extension_id),
100                map_extension_widget_slot(&handler.surface),
101                Some(Capability::AdminShellAccess),
102                None,
103            )?);
104        }
105
106        ensure_unique_widgets(&widgets)?;
107        Ok(widgets)
108    }
109
110    pub fn navigation_by_section(
111        &self,
112        operator: &OperatorAccessContext,
113    ) -> HashMap<NavigationSection, Vec<AdminResourceDescriptor>> {
114        let mut grouped = HashMap::new();
115        for resource in self.visible_resources(operator) {
116            grouped
117                .entry(resource.section)
118                .or_insert_with(Vec::new)
119                .push(resource);
120        }
121        grouped
122    }
123
124    pub fn visible_widgets(&self, operator: &OperatorAccessContext) -> Vec<AdminWidgetDescriptor> {
125        self.widgets
126            .iter()
127            .filter(|widget| {
128                widget
129                    .required_capability
130                    .is_none_or(|capability| operator.allows(capability))
131            })
132            .cloned()
133            .collect()
134    }
135
136    pub fn build_bulk_action_plan(
137        &self,
138        workflow_id: &WorkflowId,
139        resource_count: usize,
140        operator: &OperatorAccessContext,
141    ) -> Option<BulkActionPlan> {
142        let workflow = self
143            .workflows
144            .iter()
145            .find(|workflow| &workflow.id == workflow_id)?;
146        if !operator.allows(workflow.required_capability) {
147            return None;
148        }
149
150        Some(BulkActionPlan {
151            workflow_id: workflow.id.clone(),
152            resource_count,
153            message: workflow.success_message.clone(),
154        })
155    }
156
157    pub fn record_audit_entry(&mut self, entry: AuditEntry) {
158        self.audit_log.push(entry);
159    }
160
161    pub fn visible_audit_entries(&self, operator: &OperatorAccessContext) -> &[AuditEntry] {
162        if operator.allows(Capability::AdminAuditRead) {
163            &self.audit_log
164        } else {
165            &[]
166        }
167    }
168}
169
170fn ensure_unique_resources(resources: &[AdminResourceDescriptor]) -> Result<(), AdminModelError> {
171    let mut seen = HashSet::new();
172    for resource in resources {
173        if !seen.insert(resource.id.clone()) {
174            return Err(AdminModelError::DuplicateResource {
175                resource_id: resource.id.to_string(),
176            });
177        }
178    }
179    Ok(())
180}
181
182fn ensure_unique_widgets(widgets: &[AdminWidgetDescriptor]) -> Result<(), AdminModelError> {
183    let mut seen = HashSet::new();
184    for widget in widgets {
185        if !seen.insert(widget.id.clone()) {
186            return Err(AdminModelError::DuplicateWidget {
187                widget_id: widget.id.to_string(),
188            });
189        }
190    }
191    Ok(())
192}
193
194fn ensure_unique_workflows(workflows: &[WorkflowAction]) -> Result<(), AdminModelError> {
195    let mut seen = HashSet::new();
196    for workflow in workflows {
197        if !seen.insert(workflow.id.clone()) {
198            return Err(AdminModelError::DuplicateWorkflow {
199                workflow_id: workflow.id.to_string(),
200            });
201        }
202    }
203    Ok(())
204}