Skip to main content

teaql_cloud_starter/
lib.rs

1//! # teaql-cloud-starter
2//!
3//! One-line cloud bootstrap for TeaQL Rust services.
4//!
5//! Similar to Spring Boot Starter, this crate packages all cloud capabilities
6//! into a single `CloudApp` builder:
7//!
8//! - Nacos v2 gRPC connection
9//! - Service registration & discovery
10//! - Configuration center integration
11//! - Spring Boot Actuator-compatible health/info/metrics endpoints
12//! - Graceful shutdown (SIGINT/SIGTERM → readiness=OUT_OF_SERVICE → deregister → drain → exit)
13//!
14//! ## Usage
15//!
16//! ```ignore
17//! use teaql_cloud_starter::CloudApp;
18//!
19//! #[tokio::main]
20//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
21//!     CloudApp::new()
22//!         .nacos("127.0.0.1:8848")
23//!         .namespace("production")
24//!         .service_name("order-service-rust")
25//!         .port(8080)
26//!         .routes(my_business_routes())
27//!         .start()
28//!         .await?;
29//!     Ok(())
30//! }
31//! ```
32
33use std::net::SocketAddr;
34use std::sync::Arc;
35
36use axum::Router;
37
38use teaql_cloud_actuator::{ActuatorState, ServiceInfo, actuator_routes};
39use teaql_cloud_consul::{ConsulCloud, ConsulConfig};
40use teaql_cloud_core::{
41    CloudError, HealthIndicator, MetricsCollector, ServiceInstance, ServiceLifecycle,
42    ServiceRegistry, wait_for_shutdown_signal,
43};
44use teaql_cloud_nacos::{NacosCloud, NacosConfig};
45
46// Re-export key types for convenience
47pub use teaql_cloud_actuator::{self as actuator};
48pub use teaql_cloud_consul::{self as consul};
49pub use teaql_cloud_core::{self as core};
50pub use teaql_cloud_nacos::{self as nacos};
51
52pub enum RegistryType {
53    Nacos,
54    Consul,
55}
56
57/// Cloud application builder — one-line bootstrap for all cloud capabilities.
58///
59/// `CloudApp::start()` internally executes:
60/// 1. Connect to Nacos (gRPC)
61/// 2. Register service instance
62/// 3. Assemble Axum Router (business routes + actuator routes)
63/// 4. Bind port, start HTTP server
64/// 5. Listen for SIGINT/SIGTERM → graceful shutdown → deregister
65pub struct CloudApp {
66    registry_type: RegistryType,
67    registry_addr: String,
68    namespace: String,
69    service_name: String,
70    ip: String,
71    port: u16,
72    routes: Option<Router>,
73    indicators: Vec<Arc<dyn HealthIndicator>>,
74    collectors: Vec<Arc<dyn MetricsCollector>>,
75    info: ServiceInfo,
76    nacos_auth: Option<(String, String)>,
77    nacos_group: String,
78}
79
80impl CloudApp {
81    /// Create a new CloudApp builder with sensible defaults.
82    pub fn new() -> Self {
83        Self {
84            registry_type: RegistryType::Consul,
85            registry_addr: "127.0.0.1:8500".to_string(),
86            namespace: String::new(),
87            service_name: "teaql-service".to_string(),
88            ip: "0.0.0.0".to_string(),
89            port: 8080,
90            routes: None,
91            indicators: Vec::new(),
92            collectors: Vec::new(),
93            info: ServiceInfo::default(),
94            nacos_auth: None,
95            nacos_group: "DEFAULT_GROUP".to_string(),
96        }
97    }
98
99    /// Set the Nacos server address (e.g. "127.0.0.1:8848").
100    pub fn nacos(mut self, addr: impl Into<String>) -> Self {
101        self.registry_type = RegistryType::Nacos;
102        self.registry_addr = addr.into();
103        self
104    }
105
106    /// Set the Consul server address (e.g. "127.0.0.1:8500").
107    pub fn consul(mut self, addr: impl Into<String>) -> Self {
108        self.registry_type = RegistryType::Consul;
109        self.registry_addr = addr.into();
110        self
111    }
112
113    /// Set the Nacos namespace.
114    pub fn namespace(mut self, ns: impl Into<String>) -> Self {
115        self.namespace = ns.into();
116        self
117    }
118
119    /// Set the service name for registration.
120    pub fn service_name(mut self, name: impl Into<String>) -> Self {
121        let name = name.into();
122        self.info.name = name.clone();
123        self.service_name = name;
124        self
125    }
126
127    /// Set the bind IP address (default: "0.0.0.0").
128    pub fn ip(mut self, ip: impl Into<String>) -> Self {
129        self.ip = ip.into();
130        self
131    }
132
133    /// Set the HTTP port (default: 8080).
134    pub fn port(mut self, port: u16) -> Self {
135        self.port = port;
136        self
137    }
138
139    /// Set the business routes.
140    pub fn routes(mut self, routes: Router) -> Self {
141        self.routes = Some(routes);
142        self
143    }
144
145    /// Add a health indicator.
146    pub fn health_indicator(mut self, indicator: Arc<dyn HealthIndicator>) -> Self {
147        self.indicators.push(indicator);
148        self
149    }
150
151    /// Add a metrics collector.
152    pub fn metrics_collector(mut self, collector: Arc<dyn MetricsCollector>) -> Self {
153        self.collectors.push(collector);
154        self
155    }
156
157    /// Set the service version.
158    pub fn version(mut self, version: impl Into<String>) -> Self {
159        self.info.version = version.into();
160        self
161    }
162
163    /// Set the Nacos authentication credentials.
164    pub fn auth(mut self, username: impl Into<String>, password: impl Into<String>) -> Self {
165        self.nacos_auth = Some((username.into(), password.into()));
166        self
167    }
168
169    /// Set the Nacos group.
170    pub fn group(mut self, group: impl Into<String>) -> Self {
171        self.nacos_group = group.into();
172        self
173    }
174
175    /// Set the service info (build metadata).
176    pub fn service_info(mut self, info: ServiceInfo) -> Self {
177        self.info = info;
178        self
179    }
180
181    /// Start the cloud application.
182    ///
183    /// This method:
184    /// 1. Connects to Nacos via gRPC
185    /// 2. Registers the service instance
186    /// 3. Merges business routes with actuator routes
187    /// 4. Starts the HTTP server
188    /// 5. Waits for shutdown signal, then gracefully shuts down
189    ///
190    /// This method blocks until the application shuts down.
191    pub async fn start(self) -> Result<(), CloudError> {
192        // 1. Build Config and Connect based on registry type
193        let (registry, health_indicator, metrics_collector): (
194            Arc<dyn ServiceRegistry>,
195            Arc<dyn HealthIndicator>,
196            Arc<dyn MetricsCollector>,
197        ) = match self.registry_type {
198            RegistryType::Nacos => {
199                let mut nacos_config = NacosConfig::new(&self.registry_addr)
200                    .with_namespace(&self.namespace)
201                    .with_app_name(&self.service_name)
202                    .with_group(&self.nacos_group);
203
204                if let Some((username, password)) = &self.nacos_auth {
205                    nacos_config = nacos_config.with_auth(username, password);
206                }
207
208                let cloud = Arc::new(NacosCloud::connect(nacos_config).await?);
209                (cloud.clone(), cloud.clone(), cloud.clone())
210            }
211            RegistryType::Consul => {
212                let mut consul_config = ConsulConfig::new(&self.registry_addr);
213
214                if let Some((token, _)) = &self.nacos_auth {
215                    consul_config = consul_config.with_token(token);
216                }
217
218                let cloud = Arc::new(ConsulCloud::connect(consul_config).await?);
219                (cloud.clone(), cloud.clone(), cloud.clone())
220            }
221        };
222
223        // 2. Build service instance
224        let instance = ServiceInstance::new(&self.service_name, &self.ip, self.port);
225
226        // 3. Start lifecycle (register + heartbeat loop)
227        let lifecycle = ServiceLifecycle::start(registry, instance).await?;
228
229        // 4. Build actuator state — include Cloud as indicator + collector
230        let mut indicators = self.indicators;
231        indicators.push(health_indicator);
232
233        let mut collectors = self.collectors;
234        collectors.push(metrics_collector);
235
236        let actuator_state = ActuatorState {
237            indicators,
238            collectors,
239            info: self.info,
240        };
241
242        // 5. Assemble router: business routes + actuator routes
243        let app = if let Some(routes) = self.routes {
244            routes.merge(actuator_routes(actuator_state))
245        } else {
246            actuator_routes(actuator_state)
247        };
248
249        // 6. Start HTTP server with graceful shutdown
250        let addr = SocketAddr::from(([0, 0, 0, 0], self.port));
251        let listener =
252            tokio::net::TcpListener::bind(addr)
253                .await
254                .map_err(|e| CloudError::Network {
255                    source: Box::new(e),
256                })?;
257
258        axum::serve(listener, app)
259            .with_graceful_shutdown(wait_for_shutdown_signal())
260            .await
261            .map_err(|e| CloudError::Network {
262                source: Box::new(e),
263            })?;
264
265        // 7. Graceful shutdown: deregister from Nacos
266        lifecycle.shutdown().await?;
267
268        Ok(())
269    }
270}
271
272impl Default for CloudApp {
273    fn default() -> Self {
274        Self::new()
275    }
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281
282    #[test]
283    fn test_cloud_app_builder() {
284        let app = CloudApp::new()
285            .nacos("10.0.0.1:8848")
286            .namespace("production")
287            .service_name("order-service")
288            .ip("192.168.1.10")
289            .port(9090)
290            .version("1.0.0")
291            .group("MY_GROUP")
292            .auth("admin", "secret");
293
294        assert_eq!(app.registry_addr, "10.0.0.1:8848");
295        assert_eq!(app.namespace, "production");
296        assert_eq!(app.service_name, "order-service");
297        assert_eq!(app.ip, "192.168.1.10");
298        assert_eq!(app.port, 9090);
299        assert_eq!(app.info.name, "order-service");
300        assert_eq!(app.info.version, "1.0.0");
301        assert_eq!(app.nacos_group, "MY_GROUP");
302        assert!(app.nacos_auth.is_some());
303    }
304
305    #[test]
306    fn test_cloud_app_defaults() {
307        let app = CloudApp::new();
308        assert_eq!(app.registry_addr, "127.0.0.1:8500");
309        assert_eq!(app.namespace, "");
310        assert_eq!(app.port, 8080);
311        assert_eq!(app.ip, "0.0.0.0");
312        assert!(app.indicators.is_empty());
313        assert!(app.collectors.is_empty());
314        assert!(app.routes.is_none());
315    }
316
317    #[test]
318    fn test_cloud_app_default_trait() {
319        let app = CloudApp::default();
320        assert_eq!(app.port, 8080);
321    }
322
323    #[test]
324    fn test_cloud_app_with_routes() {
325        let routes = Router::new();
326        let app = CloudApp::new().routes(routes);
327        assert!(app.routes.is_some());
328    }
329}