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}