Skip to main content

myko/core/report/
registration.rs

1//! Report registration via inventory.
2
3use std::{any::Any, sync::Arc};
4
5use hyphae::{Cell, CellImmutable, MapExt};
6use serde_json::Value;
7
8use super::{
9    handler::{ReportContext, ReportHandler},
10    request::ReportRequest,
11    traits::{AnyReport, ReportParams},
12};
13use crate::{common::to_value::ToValue, request::RequestContext, server::CellServerCtx};
14
15// ─────────────────────────────────────────────────────────────────────────────
16// AnyOutput - Type-erased output for the WebSocket layer
17// ─────────────────────────────────────────────────────────────────────────────
18
19/// Type-erased report output trait.
20/// Report outputs implement this to enable serialization at the WebSocket layer.
21pub trait AnyOutput: ToValue + std::fmt::Debug + Send + Sync + 'static {
22    fn as_any(&self) -> &dyn Any;
23    fn equals(&self, other: &dyn AnyOutput) -> bool;
24}
25
26/// Blanket implementation for any type that satisfies the bounds.
27impl<T: ToValue + std::fmt::Debug + PartialEq + Send + Sync + 'static> AnyOutput for T {
28    fn as_any(&self) -> &dyn Any {
29        self
30    }
31
32    fn equals(&self, other: &dyn AnyOutput) -> bool {
33        other
34            .as_any()
35            .downcast_ref::<Self>()
36            .map(|typed| self == typed)
37            .unwrap_or(false)
38    }
39}
40
41impl PartialEq for dyn AnyOutput {
42    fn eq(&self, other: &Self) -> bool {
43        self.equals(other)
44    }
45}
46
47// ─────────────────────────────────────────────────────────────────────────────
48// Type aliases for function pointers
49// ─────────────────────────────────────────────────────────────────────────────
50
51/// Type alias for report parse function.
52pub type ReportParseFn = fn(Value) -> Result<Arc<dyn AnyReport>, anyhow::Error>;
53
54/// Type-erased cell factory for reports.
55/// Takes a typed report, registry, and host_id, returns a cell of type-erased output.
56pub type ReportCellFactory = fn(
57    Arc<dyn AnyReport>,
58    Arc<RequestContext>,
59    Arc<CellServerCtx>,
60) -> Result<Cell<Arc<dyn AnyOutput>, CellImmutable>, String>;
61
62// ─────────────────────────────────────────────────────────────────────────────
63// ReportRegistration - inventory-based registration
64// ─────────────────────────────────────────────────────────────────────────────
65
66inventory::collect!(ReportRegistration);
67
68/// Registration entry for a report type.
69/// Collected via inventory for automatic discovery.
70pub struct ReportRegistration {
71    /// Report identifier (e.g., "ServerStats")
72    pub report_id: &'static str,
73    /// Crate where this report is defined (for type_gen filtering)
74    pub crate_name: &'static str,
75    /// Output type name (e.g., "ServerStatsOutput")
76    pub output_type: &'static str,
77    /// Crate where the output type is defined
78    pub output_type_crate: &'static str,
79    /// Parse function for deserializing report from JSON
80    pub parse: ReportParseFn,
81    /// Factory for creating reactive cell from report
82    pub cell_factory: ReportCellFactory,
83}
84
85// ─────────────────────────────────────────────────────────────────────────────
86// ReportFactory - Static methods for report types
87// ─────────────────────────────────────────────────────────────────────────────
88
89/// Factory trait for creating report registration data.
90///
91/// This trait has a blanket implementation for all types implementing `ReportParams`,
92/// so user-defined reports automatically get `parse` and `cell_factory` methods.
93pub trait ReportFactory: ReportParams {
94    /// Parse JSON into this report type.
95    fn parse(value: Value) -> Result<Arc<dyn AnyReport>, anyhow::Error>;
96
97    /// Create a reactive cell for this report.
98    fn cell_factory(
99        report: Arc<dyn AnyReport>,
100        request_ctx: Arc<RequestContext>,
101        server_ctx: Arc<CellServerCtx>,
102    ) -> Result<Cell<Arc<dyn AnyOutput>, CellImmutable>, String>;
103}
104
105impl<R: ReportParams> ReportFactory for R {
106    fn parse(value: Value) -> Result<Arc<dyn AnyReport>, anyhow::Error> {
107        let report = serde_json::from_value::<ReportRequest<R>>(value)?;
108        Ok(Arc::new(report))
109    }
110
111    fn cell_factory(
112        any_report: Arc<dyn AnyReport>,
113        request_ctx: Arc<RequestContext>,
114        server_ctx: Arc<CellServerCtx>,
115    ) -> Result<Cell<Arc<dyn AnyOutput>, CellImmutable>, String> {
116        // Downcast to the ReportRequest wrapper
117        let any_ref: &dyn Any = any_report.as_ref();
118        let request: ReportRequest<R> = any_ref
119            .downcast_ref::<ReportRequest<R>>()
120            .cloned()
121            .ok_or_else(|| {
122                format!(
123                    "Failed to downcast report to ReportRequest<{}>",
124                    R::report_id_static()
125                )
126            })?;
127
128        // Create a ReportContext with host_id - report args are accessed via &self in compute
129        let ctx = ReportContext::new(request_ctx, server_ctx);
130
131        // Call the inner report's compute method
132        let report_id = R::report_id_static();
133        let cell = <R as ReportHandler>::compute(&request.report, ctx);
134
135        // Map to type-erased output for the WS/report subscription layer.
136        let report_name = format!("report:{}", report_id);
137        Ok(cell
138            .map(|output| output.clone() as Arc<dyn AnyOutput>)
139            .with_name(report_name.as_str()))
140    }
141}