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_core::{
40    CloudError, HealthIndicator, MetricsCollector, ServiceInstance, ServiceLifecycle,
41    ServiceRegistry, wait_for_shutdown_signal,
42};
43use teaql_cloud_nacos::{NacosCloud, NacosConfig};
44use teaql_cloud_consul::{ConsulCloud, ConsulConfig};
45
46// Re-export key types for convenience
47pub use teaql_cloud_actuator::{self as actuator};
48pub use teaql_cloud_core::{self as core};
49pub use teaql_cloud_nacos::{self as nacos};
50pub use teaql_cloud_consul::{self as consul};
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 =
228            ServiceLifecycle::start(registry, instance).await?;
229
230        // 4. Build actuator state — include Cloud as indicator + collector
231        let mut indicators = self.indicators;
232        indicators.push(health_indicator);
233
234        let mut collectors = self.collectors;
235        collectors.push(metrics_collector);
236
237        let actuator_state = ActuatorState {
238            indicators,
239            collectors,
240            info: self.info,
241        };
242
243        // 5. Assemble router: business routes + actuator routes
244        let app = if let Some(routes) = self.routes {
245            routes.merge(actuator_routes(actuator_state))
246        } else {
247            actuator_routes(actuator_state)
248        };
249
250        // 6. Start HTTP server with graceful shutdown
251        let addr = SocketAddr::from(([0, 0, 0, 0], self.port));
252        let listener =
253            tokio::net::TcpListener::bind(addr)
254                .await
255                .map_err(|e| CloudError::Network {
256                    source: Box::new(e),
257                })?;
258
259        axum::serve(listener, app)
260            .with_graceful_shutdown(wait_for_shutdown_signal())
261            .await
262            .map_err(|e| CloudError::Network {
263                source: Box::new(e),
264            })?;
265
266        // 7. Graceful shutdown: deregister from Nacos
267        lifecycle.shutdown().await?;
268
269        Ok(())
270    }
271}
272
273impl Default for CloudApp {
274    fn default() -> Self {
275        Self::new()
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282
283    #[test]
284    fn test_cloud_app_builder() {
285        let app = CloudApp::new()
286            .nacos("10.0.0.1:8848")
287            .namespace("production")
288            .service_name("order-service")
289            .ip("192.168.1.10")
290            .port(9090)
291            .version("1.0.0")
292            .group("MY_GROUP")
293            .auth("admin", "secret");
294
295        assert_eq!(app.registry_addr, "10.0.0.1:8848");
296        assert_eq!(app.namespace, "production");
297        assert_eq!(app.service_name, "order-service");
298        assert_eq!(app.ip, "192.168.1.10");
299        assert_eq!(app.port, 9090);
300        assert_eq!(app.info.name, "order-service");
301        assert_eq!(app.info.version, "1.0.0");
302        assert_eq!(app.nacos_group, "MY_GROUP");
303        assert!(app.nacos_auth.is_some());
304    }
305
306    #[test]
307    fn test_cloud_app_defaults() {
308        let app = CloudApp::new();
309        assert_eq!(app.registry_addr, "127.0.0.1:8500");
310        assert_eq!(app.namespace, "");
311        assert_eq!(app.port, 8080);
312        assert_eq!(app.ip, "0.0.0.0");
313        assert!(app.indicators.is_empty());
314        assert!(app.collectors.is_empty());
315        assert!(app.routes.is_none());
316    }
317
318    #[test]
319    fn test_cloud_app_default_trait() {
320        let app = CloudApp::default();
321        assert_eq!(app.port, 8080);
322    }
323
324    #[test]
325    fn test_cloud_app_with_routes() {
326        let routes = Router::new();
327        let app = CloudApp::new().routes(routes);
328        assert!(app.routes.is_some());
329    }
330}