plexus_substrate/activations/solar/
activation.rs1use 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#[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 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 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 #[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 #[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 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#[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 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 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 assert!(schema.is_hub());
177 let children = schema.children.as_ref().unwrap();
178
179 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 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 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 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 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}