1use 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
46pub 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
57pub 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 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 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 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 pub fn namespace(mut self, ns: impl Into<String>) -> Self {
115 self.namespace = ns.into();
116 self
117 }
118
119 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 pub fn ip(mut self, ip: impl Into<String>) -> Self {
129 self.ip = ip.into();
130 self
131 }
132
133 pub fn port(mut self, port: u16) -> Self {
135 self.port = port;
136 self
137 }
138
139 pub fn routes(mut self, routes: Router) -> Self {
141 self.routes = Some(routes);
142 self
143 }
144
145 pub fn health_indicator(mut self, indicator: Arc<dyn HealthIndicator>) -> Self {
147 self.indicators.push(indicator);
148 self
149 }
150
151 pub fn metrics_collector(mut self, collector: Arc<dyn MetricsCollector>) -> Self {
153 self.collectors.push(collector);
154 self
155 }
156
157 pub fn version(mut self, version: impl Into<String>) -> Self {
159 self.info.version = version.into();
160 self
161 }
162
163 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 pub fn group(mut self, group: impl Into<String>) -> Self {
171 self.nacos_group = group.into();
172 self
173 }
174
175 pub fn service_info(mut self, info: ServiceInfo) -> Self {
177 self.info = info;
178 self
179 }
180
181 pub async fn start(self) -> Result<(), CloudError> {
192 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 let instance = ServiceInstance::new(&self.service_name, &self.ip, self.port);
225
226 let lifecycle =
228 ServiceLifecycle::start(registry, instance).await?;
229
230 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 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 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 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}