Skip to main content

spark/
registry.rs

1//! Component registry — inventory-driven lookup tables.
2//!
3//! Each `#[spark::component]`-annotated struct emits a `ComponentEntry` that
4//! maps class names → constructors / hydrators. The runtime looks up by class
5//! name (FQN) when decoding a snapshot, or by short suffix (e.g. `"counter"`)
6//! when mounting a fresh component from a forge directive.
7
8use std::collections::HashMap;
9use std::pin::Pin;
10
11use futures::Future;
12use once_cell::sync::Lazy;
13use parking_lot::RwLock;
14
15use crate::component::{Ctx, MountProps, PropertyWrite};
16use crate::error::{Error, Result};
17
18/// Object-safe runtime handle for a component instance. The vtable holds all
19/// the methods Spark calls from outside the component itself. Generated by the
20/// `#[spark::component]` derive (or hand-written for `impl Trait`-shaped wrappers).
21pub trait DynComponent: Send + Sync {
22    fn as_any_mut(&mut self) -> &mut (dyn std::any::Any + Send + Sync);
23    fn snapshot_data(&self) -> serde_json::Value;
24    fn render(&self) -> Result<String>;
25    fn apply_writes<'a>(
26        &'a mut self,
27        writes: &'a [PropertyWrite],
28        ctx: &'a mut Ctx,
29    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
30    fn dispatch_call<'a>(
31        &'a mut self,
32        method: &'a str,
33        args: Vec<serde_json::Value>,
34        ctx: &'a mut Ctx,
35    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
36}
37
38/// Boxed runtime handle. `class` and `view` are pulled from the registry entry
39/// so they're available without re-querying.
40pub struct BoxedComponent {
41    pub class: &'static str,
42    pub view: &'static str,
43    pub state: Box<dyn DynComponent>,
44}
45
46/// Inventory record. Each `#[spark::component]` submits one of these via
47/// `inventory::submit!`.
48pub struct ComponentEntry {
49    pub class: &'static str,
50    pub view: &'static str,
51    pub listeners: fn() -> Vec<String>,
52    pub mount: fn(MountProps) -> BoxedComponent,
53    pub load: fn(&serde_json::Value) -> Result<BoxedComponent>,
54}
55
56inventory::collect!(ComponentEntry);
57
58/// Dispatch table entry — submitted by `#[spark_actions]`. Each component class
59/// has at most one. The dispatch function downcasts the type-erased component
60/// reference and matches the method name to the user's `fn`s.
61pub struct DispatchEntry {
62    pub class: &'static str,
63    pub dispatch: for<'a> fn(
64        component: &'a mut (dyn std::any::Any + Send + Sync),
65        method: &'a str,
66        args: Vec<serde_json::Value>,
67        ctx: &'a mut crate::component::Ctx,
68    ) -> Pin<Box<dyn futures::Future<Output = Result<()>> + Send + 'a>>,
69}
70
71inventory::collect!(DispatchEntry);
72
73/// Listeners table entry. Submitted by `#[spark_actions]` listing every
74/// `#[spark_on("event")]` method's event name. Empty if the component has none.
75pub struct ListenerEntry {
76    pub class: &'static str,
77    pub events: fn() -> Vec<String>,
78}
79
80inventory::collect!(ListenerEntry);
81
82/// Mount factory entry. Submitted by `#[spark_actions]` when a `#[spark_mount]`
83/// method is present, otherwise the component falls back to `Default::default()`.
84///
85/// The factory returns a type-erased boxed instance of the concrete component
86/// type — the caller (the component's generated `default_then_props`) downcasts
87/// back to `Self` using `Box::downcast::<Self>()`. This keeps the entry type
88/// non-generic so `inventory::collect!` works.
89pub struct MountEntry {
90    pub class: &'static str,
91    pub mount: fn(crate::component::MountProps) -> Box<dyn std::any::Any + Send>,
92}
93
94inventory::collect!(MountEntry);
95
96/// Look up the dispatcher for a class, if any.
97pub fn dispatcher_for(class: &str) -> Option<&'static DispatchEntry> {
98    inventory::iter::<DispatchEntry>
99        .into_iter()
100        .find(|e| e.class == class)
101}
102
103/// Look up the registered listeners for a class, returning the empty list if none.
104pub fn listeners_for(class: &str) -> Vec<String> {
105    for entry in inventory::iter::<ListenerEntry> {
106        if entry.class == class {
107            return (entry.events)();
108        }
109    }
110    Vec::new()
111}
112
113/// Look up the registered `#[spark_mount]` factory for a class, if any.
114pub fn mount_factory_for(
115    class: &str,
116) -> Option<fn(crate::component::MountProps) -> Box<dyn std::any::Any + Send>> {
117    for entry in inventory::iter::<MountEntry> {
118        if entry.class == class {
119            return Some(entry.mount);
120        }
121    }
122    None
123}
124
125static REGISTRY: Lazy<RwLock<HashMap<&'static str, &'static ComponentEntry>>> = Lazy::new(|| {
126    let mut map = HashMap::new();
127    for entry in inventory::iter::<ComponentEntry> {
128        map.insert(entry.class, entry);
129    }
130    RwLock::new(map)
131});
132
133/// Lookup a component entry by exact class FQN.
134pub fn lookup(class: &str) -> Option<&'static ComponentEntry> {
135    REGISTRY.read().get(class).copied()
136}
137
138/// Lookup by short name (the typical `@spark("counter", ...)` form). Picks the
139/// first registered class whose FQN ends with `::<short>` (case-insensitive).
140pub fn lookup_by_short_name(short: &str) -> Option<&'static ComponentEntry> {
141    let map = REGISTRY.read();
142
143    let suffix = format!("::{}", short);
144    if let Some(entry) = map.values().find(|e| e.class.ends_with(&suffix)) {
145        return Some(*entry);
146    }
147
148    if let Some(entry) = map.values().find(|e| e.class == short) {
149        return Some(*entry);
150    }
151
152    let lower = short.to_ascii_lowercase();
153    map.values()
154        .find(|e| e.class.to_ascii_lowercase().ends_with(&lower))
155        .copied()
156}
157
158/// Resolve a mount name to a registered entry, returning a clear error otherwise.
159pub fn resolve(name: &str) -> Result<&'static ComponentEntry> {
160    lookup(name)
161        .or_else(|| lookup_by_short_name(name))
162        .ok_or_else(|| Error::UnknownComponent(name.to_string()))
163}
164
165/// List every registered component class name (handy for diagnostics).
166pub fn classes() -> Vec<&'static str> {
167    REGISTRY.read().keys().copied().collect()
168}