1use super::traits::{Tool, ToolResult};
7use crate::util::truncate_with_ellipsis;
8use async_trait::async_trait;
9use serde::{Deserialize, Serialize};
10use serde_json::json;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct CloudPattern {
15 pub name: String,
16 pub description: String,
17 pub cloud_providers: Vec<String>,
18 pub use_case: String,
19 pub example_iac: String,
20 keywords: Vec<String>,
22}
23
24pub struct CloudPatternsTool {
26 patterns: Vec<CloudPattern>,
27}
28
29impl CloudPatternsTool {
30 pub fn new() -> Self {
31 Self {
32 patterns: built_in_patterns(),
33 }
34 }
35}
36
37#[async_trait]
38impl Tool for CloudPatternsTool {
39 fn name(&self) -> &str {
40 "cloud_patterns"
41 }
42
43 fn description(&self) -> &str {
44 "Cloud pattern library. Given a workload description, suggests applicable cloud-native \
45 architectural patterns (containerization, serverless, database modernization, etc.)."
46 }
47
48 fn parameters_schema(&self) -> serde_json::Value {
49 json!({
50 "type": "object",
51 "properties": {
52 "action": {
53 "type": "string",
54 "enum": ["match", "list"],
55 "description": "Action: 'match' to find patterns for a workload, 'list' to show all patterns."
56 },
57 "workload": {
58 "type": "string",
59 "description": "Description of the workload to match patterns against (required for 'match')."
60 },
61 "cloud": {
62 "type": "string",
63 "description": "Filter patterns by cloud provider (aws, azure, gcp). Optional."
64 }
65 },
66 "required": ["action"]
67 })
68 }
69
70 async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
71 let action = args
72 .get("action")
73 .and_then(|v| v.as_str())
74 .unwrap_or_default();
75 let workload = args
76 .get("workload")
77 .and_then(|v| v.as_str())
78 .unwrap_or_default();
79 let cloud_filter = args.get("cloud").and_then(|v| v.as_str());
80
81 match action {
82 "list" => {
83 let filtered = self.filter_by_cloud(cloud_filter);
84 let summaries: Vec<serde_json::Value> = filtered
85 .iter()
86 .map(|p| {
87 json!({
88 "name": p.name,
89 "description": p.description,
90 "cloud_providers": p.cloud_providers,
91 "use_case": p.use_case,
92 })
93 })
94 .collect();
95
96 let output = json!({
97 "patterns_count": summaries.len(),
98 "patterns": summaries,
99 });
100
101 Ok(ToolResult {
102 success: true,
103 output: serde_json::to_string_pretty(&output)?,
104 error: None,
105 })
106 }
107 "match" => {
108 if workload.trim().is_empty() {
109 return Ok(ToolResult {
110 success: false,
111 output: String::new(),
112 error: Some("'workload' parameter is required for 'match' action".into()),
113 });
114 }
115
116 let matched = self.match_patterns(workload, cloud_filter);
117
118 let output = json!({
119 "workload_summary": truncate_with_ellipsis(workload, 200),
120 "matched_count": matched.len(),
121 "matched_patterns": matched,
122 });
123
124 Ok(ToolResult {
125 success: true,
126 output: serde_json::to_string_pretty(&output)?,
127 error: None,
128 })
129 }
130 _ => Ok(ToolResult {
131 success: false,
132 output: String::new(),
133 error: Some(format!("Unknown action '{}'. Valid: match, list", action)),
134 }),
135 }
136 }
137}
138
139impl CloudPatternsTool {
140 fn filter_by_cloud(&self, cloud: Option<&str>) -> Vec<&CloudPattern> {
141 match cloud {
142 Some(c) => self
143 .patterns
144 .iter()
145 .filter(|p| p.cloud_providers.iter().any(|cp| cp == c))
146 .collect(),
147 None => self.patterns.iter().collect(),
148 }
149 }
150
151 fn match_patterns(&self, workload: &str, cloud: Option<&str>) -> Vec<serde_json::Value> {
152 let lower = workload.to_lowercase();
153 let candidates = self.filter_by_cloud(cloud);
154
155 let mut scored: Vec<(&CloudPattern, usize)> = candidates
156 .into_iter()
157 .filter_map(|p| {
158 let score: usize = p
159 .keywords
160 .iter()
161 .filter(|kw| lower.contains(kw.as_str()))
162 .count();
163 if score > 0 { Some((p, score)) } else { None }
164 })
165 .collect();
166
167 scored.sort_by(|a, b| b.1.cmp(&a.1));
168
169 let include_example = cloud.is_none() || cloud == Some("aws");
172
173 scored
174 .into_iter()
175 .map(|(p, score)| {
176 let mut entry = json!({
177 "name": p.name,
178 "description": p.description,
179 "cloud_providers": p.cloud_providers,
180 "use_case": p.use_case,
181 "relevance_score": score,
182 });
183 if include_example {
184 entry["example_iac"] = json!(p.example_iac);
185 }
186 entry
187 })
188 .collect()
189 }
190}
191
192fn built_in_patterns() -> Vec<CloudPattern> {
193 vec![
194 CloudPattern {
195 name: "containerization".into(),
196 description: "Package applications into containers for portability and consistent deployment.".into(),
197 cloud_providers: vec!["aws".into(), "azure".into(), "gcp".into()],
198 use_case: "Modernizing monolithic applications, improving deployment consistency, enabling microservices.".into(),
199 example_iac: r#"# Terraform ECS Fargate example
200resource "aws_ecs_cluster" "main" {
201 name = "app-cluster"
202}
203resource "aws_ecs_service" "app" {
204 cluster = aws_ecs_cluster.main.id
205 task_definition = aws_ecs_task_definition.app.arn
206 launch_type = "FARGATE"
207 desired_count = 2
208}"#.into(),
209 keywords: vec!["container".into(), "docker".into(), "monolith".into(), "microservice".into(), "ecs".into(), "aks".into(), "gke".into(), "kubernetes".into(), "k8s".into()],
210 },
211 CloudPattern {
212 name: "serverless_migration".into(),
213 description: "Migrate event-driven or periodic workloads to serverless compute.".into(),
214 cloud_providers: vec!["aws".into(), "azure".into(), "gcp".into()],
215 use_case: "Batch jobs, API backends, event processing, cron tasks with variable load.".into(),
216 example_iac: r#"# Terraform Lambda example
217resource "aws_lambda_function" "handler" {
218 function_name = "event-handler"
219 runtime = "python3.12"
220 handler = "main.handler"
221 filename = "handler.zip"
222 memory_size = 256
223 timeout = 30
224}"#.into(),
225 keywords: vec!["serverless".into(), "lambda".into(), "function".into(), "event".into(), "batch".into(), "cron".into(), "api".into(), "webhook".into()],
226 },
227 CloudPattern {
228 name: "database_modernization".into(),
229 description: "Migrate self-managed databases to cloud-managed services for reduced ops overhead.".into(),
230 cloud_providers: vec!["aws".into(), "azure".into(), "gcp".into()],
231 use_case: "Self-managed MySQL/PostgreSQL/SQL Server migration, NoSQL adoption, read replica scaling.".into(),
232 example_iac: r#"# Terraform RDS example
233resource "aws_db_instance" "main" {
234 engine = "postgres"
235 engine_version = "15"
236 instance_class = "db.t3.medium"
237 allocated_storage = 100
238 multi_az = true
239 backup_retention_period = 7
240 storage_encrypted = true
241}"#.into(),
242 keywords: vec!["database".into(), "mysql".into(), "postgres".into(), "sql".into(), "rds".into(), "nosql".into(), "dynamo".into(), "mongodb".into(), "migration".into()],
243 },
244 CloudPattern {
245 name: "api_gateway".into(),
246 description: "Centralize API management with rate limiting, auth, and routing.".into(),
247 cloud_providers: vec!["aws".into(), "azure".into(), "gcp".into()],
248 use_case: "Public API exposure, microservice routing, API versioning, throttling.".into(),
249 example_iac: r#"# Terraform API Gateway example
250resource "aws_apigatewayv2_api" "main" {
251 name = "app-api"
252 protocol_type = "HTTP"
253}
254resource "aws_apigatewayv2_stage" "prod" {
255 api_id = aws_apigatewayv2_api.main.id
256 name = "prod"
257 auto_deploy = true
258}"#.into(),
259 keywords: vec!["api".into(), "gateway".into(), "rest".into(), "graphql".into(), "routing".into(), "rate limit".into(), "throttl".into()],
260 },
261 CloudPattern {
262 name: "service_mesh".into(),
263 description: "Implement service mesh for observability, traffic management, and security between microservices.".into(),
264 cloud_providers: vec!["aws".into(), "azure".into(), "gcp".into()],
265 use_case: "Microservice communication, mTLS, traffic splitting, canary deployments.".into(),
266 example_iac: r#"# AWS App Mesh example
267resource "aws_appmesh_mesh" "main" {
268 name = "app-mesh"
269}
270resource "aws_appmesh_virtual_service" "app" {
271 name = "app.local"
272 mesh_name = aws_appmesh_mesh.main.name
273}"#.into(),
274 keywords: vec!["mesh".into(), "istio".into(), "envoy".into(), "sidecar".into(), "mtls".into(), "canary".into(), "traffic".into(), "microservice".into()],
275 },
276 ]
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282
283 #[test]
284 fn built_in_patterns_are_populated() {
285 let patterns = built_in_patterns();
286 assert_eq!(patterns.len(), 5);
287 let names: Vec<&str> = patterns.iter().map(|p| p.name.as_str()).collect();
288 assert!(names.contains(&"containerization"));
289 assert!(names.contains(&"serverless_migration"));
290 assert!(names.contains(&"database_modernization"));
291 assert!(names.contains(&"api_gateway"));
292 assert!(names.contains(&"service_mesh"));
293 }
294
295 #[tokio::test]
296 async fn match_returns_containerization_for_monolith() {
297 let tool = CloudPatternsTool::new();
298 let result = tool
299 .execute(json!({
300 "action": "match",
301 "workload": "We have a monolith Java application running on VMs that we want to containerize."
302 }))
303 .await
304 .unwrap();
305
306 assert!(result.success);
307 assert!(result.output.contains("containerization"));
308 }
309
310 #[tokio::test]
311 async fn match_returns_serverless_for_batch_workload() {
312 let tool = CloudPatternsTool::new();
313 let result = tool
314 .execute(json!({
315 "action": "match",
316 "workload": "Batch processing cron jobs that handle event data"
317 }))
318 .await
319 .unwrap();
320
321 assert!(result.success);
322 assert!(result.output.contains("serverless_migration"));
323 }
324
325 #[tokio::test]
326 async fn match_filters_by_cloud_provider() {
327 let tool = CloudPatternsTool::new();
328 let result = tool
329 .execute(json!({
330 "action": "match",
331 "workload": "Container deployment with Kubernetes",
332 "cloud": "aws"
333 }))
334 .await
335 .unwrap();
336
337 assert!(result.success);
338 assert!(result.output.contains("containerization"));
339 }
340
341 #[tokio::test]
342 async fn list_returns_all_patterns() {
343 let tool = CloudPatternsTool::new();
344 let result = tool
345 .execute(json!({
346 "action": "list"
347 }))
348 .await
349 .unwrap();
350
351 assert!(result.success);
352 assert!(result.output.contains("\"patterns_count\": 5"));
353 }
354
355 #[tokio::test]
356 async fn match_with_empty_workload_returns_error() {
357 let tool = CloudPatternsTool::new();
358 let result = tool
359 .execute(json!({
360 "action": "match",
361 "workload": ""
362 }))
363 .await
364 .unwrap();
365
366 assert!(!result.success);
367 assert!(result.error.is_some());
368 }
369
370 #[tokio::test]
371 async fn match_database_workload_finds_db_modernization() {
372 let tool = CloudPatternsTool::new();
373 let result = tool
374 .execute(json!({
375 "action": "match",
376 "workload": "Self-hosted PostgreSQL database needs migration to managed service"
377 }))
378 .await
379 .unwrap();
380
381 assert!(result.success);
382 assert!(result.output.contains("database_modernization"));
383 }
384
385 #[test]
386 fn pattern_matching_scores_correctly() {
387 let tool = CloudPatternsTool::new();
388 let matches =
389 tool.match_patterns("microservice container docker kubernetes deployment", None);
390 assert!(!matches.is_empty());
392 assert_eq!(matches[0]["name"], "containerization");
393 }
394
395 #[tokio::test]
396 async fn unknown_action_returns_error() {
397 let tool = CloudPatternsTool::new();
398 let result = tool
399 .execute(json!({
400 "action": "deploy"
401 }))
402 .await
403 .unwrap();
404
405 assert!(!result.success);
406 assert!(result.error.unwrap().contains("Unknown action"));
407 }
408}