Skip to main content

mockforge_vbr/
api_generator.rs

1//! CRUD API generator
2//!
3//! This module generates CRUD endpoints for each entity, including:
4//! - GET /api/{entity} - List all (with pagination, filtering, sorting)
5//! - GET /api/{entity}/{id} - Get by ID
6//! - POST /api/{entity} - Create new
7//! - PUT /api/{entity}/{id} - Full update
8//! - PATCH /api/{entity}/{id} - Partial update
9//! - DELETE /api/{entity}/{id} - Delete
10//! - GET /api/{entity}/{id}/{relationship} - Get related entities
11
12use crate::entities::Entity;
13
14/// API endpoint definition
15#[derive(Debug, Clone)]
16pub struct ApiEndpoint {
17    /// HTTP method
18    pub method: String,
19    /// Path pattern
20    pub path: String,
21    /// Handler function name
22    pub handler_name: String,
23    /// Entity name
24    pub entity_name: String,
25}
26
27/// API generator for creating CRUD endpoints
28pub struct ApiGenerator {
29    /// API prefix (e.g., "/api")
30    pub prefix: String,
31}
32
33impl ApiGenerator {
34    /// Create a new API generator
35    pub fn new(prefix: String) -> Self {
36        Self { prefix }
37    }
38
39    /// Generate all CRUD endpoints for an entity
40    pub fn generate_endpoints(&self, entity: &Entity) -> Vec<ApiEndpoint> {
41        let entity_name = entity.name().to_lowercase();
42        let mut endpoints = Vec::new();
43
44        // GET /api/{entity} - List all
45        endpoints.push(ApiEndpoint {
46            method: "GET".to_string(),
47            path: format!("{}/{}", self.prefix, entity_name),
48            handler_name: format!("list_{}", entity_name),
49            entity_name: entity.name().to_string(),
50        });
51
52        // GET /api/{entity}/{id} - Get by ID
53        endpoints.push(ApiEndpoint {
54            method: "GET".to_string(),
55            path: format!("{}/{}/{{id}}", self.prefix, entity_name),
56            handler_name: format!("get_{}", entity_name),
57            entity_name: entity.name().to_string(),
58        });
59
60        // POST /api/{entity} - Create
61        endpoints.push(ApiEndpoint {
62            method: "POST".to_string(),
63            path: format!("{}/{}", self.prefix, entity_name),
64            handler_name: format!("create_{}", entity_name),
65            entity_name: entity.name().to_string(),
66        });
67
68        // PUT /api/{entity}/{id} - Full update
69        endpoints.push(ApiEndpoint {
70            method: "PUT".to_string(),
71            path: format!("{}/{}/{{id}}", self.prefix, entity_name),
72            handler_name: format!("update_{}", entity_name),
73            entity_name: entity.name().to_string(),
74        });
75
76        // PATCH /api/{entity}/{id} - Partial update
77        endpoints.push(ApiEndpoint {
78            method: "PATCH".to_string(),
79            path: format!("{}/{}/{{id}}", self.prefix, entity_name),
80            handler_name: format!("patch_{}", entity_name),
81            entity_name: entity.name().to_string(),
82        });
83
84        // DELETE /api/{entity}/{id} - Delete
85        endpoints.push(ApiEndpoint {
86            method: "DELETE".to_string(),
87            path: format!("{}/{}/{{id}}", self.prefix, entity_name),
88            handler_name: format!("delete_{}", entity_name),
89            entity_name: entity.name().to_string(),
90        });
91
92        // Generate relationship endpoints
93        for fk in &entity.schema.foreign_keys {
94            let relationship_name = fk.field.trim_end_matches("_id");
95            endpoints.push(ApiEndpoint {
96                method: "GET".to_string(),
97                path: format!("{}/{}/{{id}}/{}", self.prefix, entity_name, relationship_name),
98                handler_name: format!(
99                    "get_{}_by_{}",
100                    fk.target_entity.to_lowercase(),
101                    relationship_name
102                ),
103                entity_name: entity.name().to_string(),
104            });
105        }
106
107        endpoints
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use crate::entities::Entity;
115    use crate::schema::VbrSchemaDefinition;
116    use mockforge_data::SchemaDefinition;
117
118    fn create_test_entity(name: &str) -> Entity {
119        let base_schema = SchemaDefinition::new(name.to_string());
120        let vbr_schema = VbrSchemaDefinition::new(base_schema);
121        Entity::new(name.to_string(), vbr_schema)
122    }
123
124    fn create_entity_with_foreign_key(name: &str) -> Entity {
125        let base_schema = SchemaDefinition::new(name.to_string());
126        let mut vbr_schema = VbrSchemaDefinition::new(base_schema);
127        vbr_schema.foreign_keys.push(crate::schema::ForeignKeyDefinition {
128            field: "user_id".to_string(),
129            target_entity: "User".to_string(),
130            target_field: "id".to_string(),
131            on_delete: crate::schema::CascadeAction::Cascade,
132            on_update: crate::schema::CascadeAction::NoAction,
133        });
134        Entity::new(name.to_string(), vbr_schema)
135    }
136
137    #[test]
138    fn test_api_generator_new() {
139        let generator = ApiGenerator::new("/api".to_string());
140        assert_eq!(generator.prefix, "/api");
141    }
142
143    #[test]
144    fn test_api_generator_custom_prefix() {
145        let generator = ApiGenerator::new("/v2/api".to_string());
146        assert_eq!(generator.prefix, "/v2/api");
147    }
148
149    #[test]
150    fn test_generate_endpoints_count() {
151        let generator = ApiGenerator::new("/api".to_string());
152        let entity = create_test_entity("User");
153        let endpoints = generator.generate_endpoints(&entity);
154
155        // Should generate 6 basic CRUD endpoints
156        assert_eq!(endpoints.len(), 6);
157    }
158
159    #[test]
160    fn test_generate_endpoints_methods() {
161        let generator = ApiGenerator::new("/api".to_string());
162        let entity = create_test_entity("User");
163        let endpoints = generator.generate_endpoints(&entity);
164
165        let methods: Vec<&str> = endpoints.iter().map(|e| e.method.as_str()).collect();
166        assert!(methods.contains(&"GET"));
167        assert!(methods.contains(&"POST"));
168        assert!(methods.contains(&"PUT"));
169        assert!(methods.contains(&"PATCH"));
170        assert!(methods.contains(&"DELETE"));
171    }
172
173    #[test]
174    fn test_generate_endpoints_paths() {
175        let generator = ApiGenerator::new("/api".to_string());
176        let entity = create_test_entity("User");
177        let endpoints = generator.generate_endpoints(&entity);
178
179        let paths: Vec<&str> = endpoints.iter().map(|e| e.path.as_str()).collect();
180        assert!(paths.contains(&"/api/user"));
181        assert!(paths.contains(&"/api/user/{id}"));
182    }
183
184    #[test]
185    fn test_generate_endpoints_handler_names() {
186        let generator = ApiGenerator::new("/api".to_string());
187        let entity = create_test_entity("User");
188        let endpoints = generator.generate_endpoints(&entity);
189
190        let handlers: Vec<&str> = endpoints.iter().map(|e| e.handler_name.as_str()).collect();
191        assert!(handlers.contains(&"list_user"));
192        assert!(handlers.contains(&"get_user"));
193        assert!(handlers.contains(&"create_user"));
194        assert!(handlers.contains(&"update_user"));
195        assert!(handlers.contains(&"patch_user"));
196        assert!(handlers.contains(&"delete_user"));
197    }
198
199    #[test]
200    fn test_generate_endpoints_entity_name() {
201        let generator = ApiGenerator::new("/api".to_string());
202        let entity = create_test_entity("Product");
203        let endpoints = generator.generate_endpoints(&entity);
204
205        for endpoint in endpoints {
206            assert_eq!(endpoint.entity_name, "Product");
207        }
208    }
209
210    #[test]
211    fn test_generate_endpoints_with_foreign_key() {
212        let generator = ApiGenerator::new("/api".to_string());
213        let entity = create_entity_with_foreign_key("Order");
214        let endpoints = generator.generate_endpoints(&entity);
215
216        // Should have 6 basic + 1 relationship endpoint
217        assert_eq!(endpoints.len(), 7);
218
219        // Check for the relationship endpoint
220        let relationship_endpoint = endpoints.iter().find(|e| e.path.contains("/user"));
221        assert!(relationship_endpoint.is_some());
222        assert_eq!(relationship_endpoint.unwrap().method, "GET");
223    }
224
225    #[test]
226    fn test_api_endpoint_debug() {
227        let endpoint = ApiEndpoint {
228            method: "GET".to_string(),
229            path: "/api/test".to_string(),
230            handler_name: "test_handler".to_string(),
231            entity_name: "Test".to_string(),
232        };
233
234        let debug = format!("{:?}", endpoint);
235        assert!(debug.contains("ApiEndpoint"));
236        assert!(debug.contains("GET"));
237        assert!(debug.contains("/api/test"));
238    }
239
240    #[test]
241    fn test_api_endpoint_clone() {
242        let endpoint = ApiEndpoint {
243            method: "POST".to_string(),
244            path: "/api/test".to_string(),
245            handler_name: "create_test".to_string(),
246            entity_name: "Test".to_string(),
247        };
248
249        let cloned = endpoint.clone();
250        assert_eq!(endpoint.method, cloned.method);
251        assert_eq!(endpoint.path, cloned.path);
252        assert_eq!(endpoint.handler_name, cloned.handler_name);
253        assert_eq!(endpoint.entity_name, cloned.entity_name);
254    }
255
256    #[test]
257    fn test_lowercase_entity_in_path() {
258        let generator = ApiGenerator::new("/api".to_string());
259        let entity = create_test_entity("UserProfile");
260        let endpoints = generator.generate_endpoints(&entity);
261
262        // Path should use lowercase entity name
263        let list_endpoint = endpoints.iter().find(|e| e.handler_name == "list_userprofile");
264        assert!(list_endpoint.is_some());
265        assert_eq!(list_endpoint.unwrap().path, "/api/userprofile");
266    }
267}