Skip to main content

plexus_substrate/activations/solar/
activation.rs

1//! Solar system activation - demonstrates nested plugin hierarchy
2//!
3//! This activation shows the coalgebraic plugin structure where plugins
4//! can have children. The solar system is a natural hierarchy:
5//! - Sol (star) contains planets
6//! - Planets contain moons
7//!
8//! Each level implements the F-coalgebra structure map via `plugin_schema()`.
9
10use super::celestial::{build_solar_system, CelestialBody, CelestialBodyActivation};
11use super::types::{BodyType, SolarEvent};
12use crate::plexus::{Activation, ChildRouter, ChildSummary};
13use async_stream::stream;
14use async_trait::async_trait;
15use futures::Stream;
16
17/// Solar system activation - demonstrates nested plugin children
18#[derive(Clone)]
19pub struct Solar {
20    system: CelestialBody,
21}
22
23impl Solar {
24    pub fn new() -> Self {
25        Self {
26            system: build_solar_system(),
27        }
28    }
29
30    /// Find a body by path (e.g., "earth" or "jupiter.io")
31    fn find_body(&self, path: &str) -> Option<&CelestialBody> {
32        let parts: Vec<&str> = path.split('.').collect();
33        let mut current = &self.system;
34
35        for part in parts {
36            let normalized = part.to_lowercase();
37            if current.name.to_lowercase() == normalized {
38                continue;
39            }
40            current = current.children.iter()
41                .find(|c| c.name.to_lowercase() == normalized)?;
42        }
43        Some(current)
44    }
45
46    /// Count all moons in the system
47    fn moon_count(&self) -> usize {
48        fn count_moons(body: &CelestialBody) -> usize {
49            let mine: usize = body.children.iter()
50                .filter(|c| c.body_type == BodyType::Moon)
51                .count();
52            let nested: usize = body.children.iter()
53                .map(count_moons)
54                .sum();
55            mine + nested
56        }
57        count_moons(&self.system)
58    }
59}
60
61impl Default for Solar {
62    fn default() -> Self {
63        Self::new()
64    }
65}
66
67#[plexus_macros::hub_methods(
68    namespace = "solar",
69    version = "1.0.0",
70    description = "Solar system model - demonstrates nested plugin hierarchy",
71    hub
72)]
73impl Solar {
74    /// Observe the entire solar system
75    #[plexus_macros::hub_method(
76        description = "Get an overview of the solar system"
77    )]
78    async fn observe(&self) -> impl Stream<Item = SolarEvent> + Send + 'static {
79        let star = self.system.name.clone();
80        let planet_count = self.system.children.len();
81        let moon_count = self.moon_count();
82        let total_bodies = 1 + self.system.descendant_count();
83
84        stream! {
85            yield SolarEvent::System {
86                star,
87                planet_count,
88                moon_count,
89                total_bodies,
90            };
91        }
92    }
93
94    /// Get information about a specific celestial body
95    #[plexus_macros::hub_method(
96        description = "Get detailed information about a celestial body",
97        params(path = "Path to the body (e.g., 'earth', 'jupiter.io', 'saturn.titan')")
98    )]
99    async fn info(
100        &self,
101        path: String,
102    ) -> impl Stream<Item = SolarEvent> + Send + 'static {
103        let body = self.find_body(&path).cloned();
104
105        stream! {
106            if let Some(b) = body {
107                yield SolarEvent::Body {
108                    name: b.name,
109                    body_type: b.body_type,
110                    mass_kg: b.mass_kg,
111                    radius_km: b.radius_km,
112                    orbital_period_days: b.orbital_period_days,
113                    parent: b.parent,
114                };
115            }
116        }
117    }
118
119    /// Get child plugin summaries (planets as children of solar system)
120    pub fn plugin_children(&self) -> Vec<ChildSummary> {
121        self.system.children.iter()
122            .map(|planet| planet.to_child_summary())
123            .collect()
124    }
125}
126
127/// ChildRouter implementation for nested method routing
128///
129/// This enables calls like `solar.mercury.info` to route through
130/// Solar → Mercury → info method.
131#[async_trait]
132impl ChildRouter for Solar {
133    fn router_namespace(&self) -> &str {
134        "solar"
135    }
136
137    async fn router_call(&self, method: &str, params: serde_json::Value) -> Result<crate::plexus::PlexusStream, crate::plexus::PlexusError> {
138        // Delegate to Activation::call which handles local methods + nested routing
139        Activation::call(self, method, params).await
140    }
141
142    async fn get_child(&self, name: &str) -> Option<Box<dyn ChildRouter>> {
143        let normalized = name.to_lowercase();
144        self.system.children.iter()
145            .find(|c| c.name.to_lowercase() == normalized)
146            .map(|c| Box::new(CelestialBodyActivation::new(c.clone())) as Box<dyn ChildRouter>)
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153    use crate::plexus::{Activation, DynamicHub};
154
155    #[test]
156    fn solar_is_hub_with_planets() {
157        let solar = Solar::new();
158        let schema = solar.plugin_schema();
159
160        assert!(schema.is_hub(), "solar should be a hub");
161        let children = schema.children.as_ref().expect("solar should have children");
162        assert_eq!(children.len(), 8, "solar should have 8 planets");
163
164        // Children are summaries - check namespace and description
165        let jupiter = children.iter().find(|c| c.namespace == "jupiter").unwrap();
166        assert!(jupiter.description.contains("planet"));
167        assert!(!jupiter.hash.is_empty());
168    }
169
170    #[test]
171    fn solar_registered_with_dynamic_hub() {
172        let hub = DynamicHub::new("plexus").register(Solar::new());
173        let schema = hub.plugin_schema();
174
175        // DynamicHub is a hub
176        assert!(schema.is_hub());
177        let children = schema.children.as_ref().unwrap();
178
179        // Solar should be one of the children (as a summary)
180        let solar = children.iter().find(|c| c.namespace == "solar").unwrap();
181        assert!(solar.description.contains("Solar system"));
182        assert!(!solar.hash.is_empty());
183    }
184
185    #[test]
186    fn solar_hash_changes_with_structure() {
187        let solar1 = Solar::new();
188        let solar2 = Solar::new();
189
190        // Same structure = same hash
191        assert_eq!(
192            solar1.plugin_schema().hash,
193            solar2.plugin_schema().hash
194        );
195    }
196
197    #[test]
198    fn print_solar_schema() {
199        let solar = Solar::new();
200        let schema = solar.plugin_schema();
201        let json = serde_json::to_string_pretty(&schema).unwrap();
202        println!("Solar system schema:\n{}", json);
203    }
204
205    #[tokio::test]
206    async fn test_nested_routing_mercury() {
207        let solar = Solar::new();
208        let result = Activation::call(&solar, "mercury.info", serde_json::json!({})).await;
209        assert!(result.is_ok(), "mercury.info should be callable: {:?}", result.err());
210    }
211
212    #[tokio::test]
213    async fn test_nested_routing_jupiter_io() {
214        let solar = Solar::new();
215
216        // Call solar.call("jupiter.io.info", {}) - should route jupiter → io
217        let result = Activation::call(&solar, "jupiter.io.info", serde_json::json!({})).await;
218        assert!(result.is_ok(), "jupiter.io.info should be callable");
219    }
220
221    #[tokio::test]
222    async fn test_nested_routing_earth_luna() {
223        let solar = Solar::new();
224
225        // Call solar.call("earth.luna.info", {}) - should route earth → luna
226        let result = Activation::call(&solar, "earth.luna.info", serde_json::json!({})).await;
227        assert!(result.is_ok(), "earth.luna.info should be callable");
228    }
229
230    #[tokio::test]
231    async fn test_nested_routing_invalid_child() {
232        let solar = Solar::new();
233
234        // Call with invalid child
235        let result = Activation::call(&solar, "pluto.info", serde_json::json!({})).await;
236        assert!(result.is_err(), "pluto.info should fail - not a planet");
237    }
238}