Skip to main content

construct/tools/
cloud_patterns.rs

1//! Cloud pattern library for recommending cloud-native architectural patterns.
2//!
3//! Provides a built-in set of cloud migration and modernization patterns,
4//! with pattern matching against workload descriptions.
5
6use 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/// A cloud architecture pattern with metadata.
13#[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 for matching against workload descriptions.
21    keywords: Vec<String>,
22}
23
24/// Tool that suggests cloud patterns given a workload description.
25pub 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        // Built-in IaC examples are AWS Terraform only; include them only when
170        // the cloud filter is unset or explicitly "aws".
171        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        // containerization should rank highest (most keyword matches)
391        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}