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::activation(namespace = "solar",
68version = "1.0.0",
69description = "Solar system model - demonstrates nested plugin hierarchy",
70hub, crate_path = "plexus_core")]
71impl Solar {
72    /// Observe the entire solar system
73    #[plexus_macros::method(description = "Get an overview of the solar system")]
74    async fn observe(&self) -> impl Stream<Item = SolarEvent> + Send + 'static {
75        let star = self.system.name.clone();
76        let planet_count = self.system.children.len();
77        let moon_count = self.moon_count();
78        let total_bodies = 1 + self.system.descendant_count();
79
80        stream! {
81            yield SolarEvent::System {
82                star,
83                planet_count,
84                moon_count,
85                total_bodies,
86            };
87        }
88    }
89
90    /// Get information about a specific celestial body
91    #[plexus_macros::method(description = "Get detailed information about a celestial body",
92    params(path = "Path to the body (e.g., 'earth', 'jupiter.io', 'saturn.titan')"))]
93    async fn info(
94        &self,
95        path: String,
96    ) -> impl Stream<Item = SolarEvent> + Send + 'static {
97        let body = self.find_body(&path).cloned();
98
99        stream! {
100            if let Some(b) = body {
101                yield SolarEvent::Body {
102                    name: b.name,
103                    body_type: b.body_type,
104                    mass_kg: b.mass_kg,
105                    radius_km: b.radius_km,
106                    orbital_period_days: b.orbital_period_days,
107                    parent: b.parent,
108                };
109            }
110        }
111    }
112
113    /// Get child plugin summaries (planets as children of solar system)
114    pub fn plugin_children(&self) -> Vec<ChildSummary> {
115        self.system.children.iter()
116            .map(|planet| planet.to_child_summary())
117            .collect()
118    }
119}
120
121/// ChildRouter implementation for nested method routing
122///
123/// This enables calls like `solar.mercury.info` to route through
124/// Solar → Mercury → info method.
125#[async_trait]
126impl ChildRouter for Solar {
127    fn router_namespace(&self) -> &str {
128        "solar"
129    }
130
131    async fn router_call(&self, method: &str, params: serde_json::Value, auth: Option<&plexus_core::plexus::AuthContext>, raw_ctx: Option<&plexus_core::request::RawRequestContext>) -> Result<crate::plexus::PlexusStream, crate::plexus::PlexusError> {
132            // Delegate to Activation::call which handles local methods + nested routing
133            Activation::call(self, method, params, auth, raw_ctx).await
134        }
135
136    async fn get_child(&self, name: &str) -> Option<Box<dyn ChildRouter>> {
137        let normalized = name.to_lowercase();
138        self.system.children.iter()
139            .find(|c| c.name.to_lowercase() == normalized)
140            .map(|c| Box::new(CelestialBodyActivation::new(c.clone())) as Box<dyn ChildRouter>)
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use crate::plexus::{Activation, DynamicHub};
148
149    #[test]
150    fn solar_is_hub_with_planets() {
151        let solar = Solar::new();
152        let schema = solar.plugin_schema();
153
154        assert!(schema.is_hub(), "solar should be a hub");
155        let children = schema.children.as_ref().expect("solar should have children");
156        assert_eq!(children.len(), 8, "solar should have 8 planets");
157
158        // Children are summaries - check namespace and description
159        let jupiter = children.iter().find(|c| c.namespace == "jupiter").unwrap();
160        assert!(jupiter.description.contains("planet"));
161        assert!(!jupiter.hash.is_empty());
162    }
163
164    #[test]
165    fn solar_registered_with_dynamic_hub() {
166        let hub = DynamicHub::new("plexus").register(Solar::new());
167        let schema = hub.plugin_schema();
168
169        // DynamicHub is a hub
170        assert!(schema.is_hub());
171        let children = schema.children.as_ref().unwrap();
172
173        // Solar should be one of the children (as a summary)
174        let solar = children.iter().find(|c| c.namespace == "solar").unwrap();
175        assert!(solar.description.contains("Solar system"));
176        assert!(!solar.hash.is_empty());
177    }
178
179    #[test]
180    fn solar_hash_changes_with_structure() {
181        let solar1 = Solar::new();
182        let solar2 = Solar::new();
183
184        // Same structure = same hash
185        assert_eq!(
186            solar1.plugin_schema().hash,
187            solar2.plugin_schema().hash
188        );
189    }
190
191    #[test]
192    fn print_solar_schema() {
193        let solar = Solar::new();
194        let schema = solar.plugin_schema();
195        let json = serde_json::to_string_pretty(&schema).unwrap();
196        println!("Solar system schema:\n{}", json);
197    }
198
199    #[tokio::test]
200    async fn test_nested_routing_mercury() {
201        let solar = Solar::new();
202        let result = Activation::call(&solar, "mercury.info", serde_json::json!({})).await;
203        assert!(result.is_ok(), "mercury.info should be callable: {:?}", result.err());
204    }
205
206    #[tokio::test]
207    async fn test_nested_routing_jupiter_io() {
208        let solar = Solar::new();
209
210        // Call solar.call("jupiter.io.info", {}) - should route jupiter → io
211        let result = Activation::call(&solar, "jupiter.io.info", serde_json::json!({})).await;
212        assert!(result.is_ok(), "jupiter.io.info should be callable");
213    }
214
215    #[tokio::test]
216    async fn test_nested_routing_earth_luna() {
217        let solar = Solar::new();
218
219        // Call solar.call("earth.luna.info", {}) - should route earth → luna
220        let result = Activation::call(&solar, "earth.luna.info", serde_json::json!({})).await;
221        assert!(result.is_ok(), "earth.luna.info should be callable");
222    }
223
224    #[tokio::test]
225    async fn test_nested_routing_invalid_child() {
226        let solar = Solar::new();
227
228        // Call with invalid child
229        let result = Activation::call(&solar, "pluto.info", serde_json::json!({})).await;
230        assert!(result.is_err(), "pluto.info should fail - not a planet");
231    }
232}