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::activation(namespace = "solar",
68version = "1.0.0",
69description = "Solar system model - demonstrates nested plugin hierarchy",
70hub, crate_path = "plexus_core")]
71impl Solar {
72 #[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 #[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 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#[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 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 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 assert!(schema.is_hub());
171 let children = schema.children.as_ref().unwrap();
172
173 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 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 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 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 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}