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