mockforge_grpc/dynamic/http_bridge/
route_generator.rs

1//! Route generation for HTTP bridge
2//!
3//! This module generates HTTP routes from protobuf service definitions,
4//! mapping gRPC methods to RESTful endpoints.
5
6use super::HttpBridgeConfig;
7use regex::Regex;
8
9/// Generates HTTP routes from protobuf service definitions
10#[derive(Debug, Clone)]
11pub struct RouteGenerator {
12    /// Configuration for route generation
13    config: HttpBridgeConfig,
14    /// Regular expression for cleaning service/service names for HTTP paths
15    service_name_regex: Regex,
16    /// Regular expression for cleaning method names for HTTP paths
17    method_name_regex: Regex,
18}
19
20impl RouteGenerator {
21    /// Create a new route generator
22    pub fn new(config: HttpBridgeConfig) -> Self {
23        Self {
24            config,
25            service_name_regex: Regex::new(r"[^a-zA-Z0-9_.-]").unwrap(),
26            method_name_regex: Regex::new(r"[^a-zA-Z0-9_.-]").unwrap(),
27        }
28    }
29
30    /// Generate HTTP route path for a service method
31    pub fn generate_route_path(&self, service_name: &str, method_name: &str) -> String {
32        let clean_service = self.clean_service_name(service_name);
33        let clean_method = self.clean_method_name(method_name);
34
35        format!("{}{}", self.config.base_path, self.config.route_pattern)
36            .replace("{service}", &clean_service)
37            .replace("{method}", &clean_method)
38    }
39
40    /// Generate URL pattern from route path
41    pub fn generate_url_pattern(&self, _service_name: &str, _method_name: &str) -> String {
42        let template = format!("{}{}", self.config.base_path, self.config.route_pattern);
43        // Replace path parameters with regex patterns
44        template.replace("{service}", "[^/]+").replace("{method}", "[^/]+")
45    }
46
47    /// Clean service name for HTTP routing
48    pub fn clean_service_name(&self, service_name: &str) -> String {
49        // Remove package prefix if present
50        let without_package = if let Some(dot_index) = service_name.rfind('.') {
51            &service_name[dot_index + 1..]
52        } else {
53            service_name
54        };
55
56        // Replace non-alphanumeric characters with dashes
57        let cleaned = self.service_name_regex.replace_all(without_package, "-");
58
59        // Convert to lowercase for consistency
60        cleaned.to_lowercase()
61    }
62
63    /// Clean method name for HTTP routing
64    pub fn clean_method_name(&self, method_name: &str) -> String {
65        // Replace non-alphanumeric characters with dashes
66        let cleaned = self.method_name_regex.replace_all(method_name, "-");
67
68        // Convert to lowercase for consistency
69        cleaned.to_lowercase()
70    }
71
72    /// Generate OpenAPI path specification for a service method
73    pub fn generate_openapi_path(
74        &self,
75        service_name: &str,
76        method_name: &str,
77        method_descriptor: &prost_reflect::MethodDescriptor,
78    ) -> serde_json::Value {
79        let path = self.generate_route_path(service_name, method_name);
80        let operation =
81            self.generate_openapi_operation(service_name, method_name, method_descriptor);
82
83        serde_json::json!({ path: operation })
84    }
85
86    /// Generate full OpenAPI specification for all services
87    pub fn generate_openapi_spec(
88        &self,
89        services: &std::collections::HashMap<String, crate::dynamic::proto_parser::ProtoService>,
90    ) -> serde_json::Value {
91        let mut paths = serde_json::Map::new();
92        let mut schemas = serde_json::Map::new();
93
94        // Generate paths for all services and methods
95        for (service_name, service) in services {
96            for method in &service.methods {
97                let path = self.generate_route_path(service_name, &method.name);
98                let operation = self.generate_openapi_operation_full(
99                    service_name,
100                    &method.name,
101                    method,
102                    &mut schemas,
103                );
104
105                // Determine HTTP method
106                let http_method = if method.server_streaming {
107                    "get"
108                } else {
109                    "post"
110                };
111
112                paths.insert(
113                    path,
114                    serde_json::json!({
115                        http_method: operation
116                    }),
117                );
118            }
119        }
120
121        serde_json::json!({
122            "openapi": "3.0.1",
123            "info": {
124                "title": "MockForge gRPC HTTP Bridge API",
125                "description": "RESTful API bridge to gRPC services automatically generated from protobuf definitions",
126                "version": "1.0.0",
127                "contact": {
128                    "name": "MockForge",
129                    "url": "https://github.com/SaaSy-Solutions/mockforge"
130                },
131                "license": {
132                    "name": "MIT",
133                    "url": "https://opensource.org/licenses/MIT"
134                }
135            },
136            "servers": [{
137                "url": "http://localhost:9080",
138                "description": "Local development server"
139            }],
140            "security": [],
141            "paths": paths,
142            "components": {
143                "schemas": schemas,
144                "securitySchemes": {}
145            }
146        })
147    }
148
149    /// Generate complete OpenAPI operation with schema references
150    fn generate_openapi_operation_full(
151        &self,
152        service_name: &str,
153        method_name: &str,
154        proto_method: &crate::dynamic::proto_parser::ProtoMethod,
155        schemas: &mut serde_json::Map<String, serde_json::Value>,
156    ) -> serde_json::Value {
157        let mut operation = serde_json::json!({
158            "summary": format!("{} {}", method_name, service_name),
159            "description": format!("Calls the {} method on {} service\n\n**gRPC Method Details:**\n- Service: {}\n- Method: {}\n- Input: {}\n- Output: {}\n{}",
160                method_name,
161                service_name,
162                service_name,
163                method_name,
164                proto_method.input_type,
165                proto_method.output_type,
166                if proto_method.server_streaming {
167                    "\n- Server Streaming: Yes (returns SSE stream)"
168                } else {
169                    "\n- Server Streaming: No"
170                }
171            ),
172            "tags": [service_name],
173            "parameters": self.generate_openapi_parameters(),
174        });
175
176        // Add request body for methods that send data
177        if !proto_method.server_streaming {
178            operation["requestBody"] =
179                self.generate_openapi_request_body_full(proto_method, schemas);
180        }
181
182        // Add responses
183        operation["responses"] =
184            self.generate_openapi_responses_full(service_name, method_name, proto_method, schemas);
185
186        operation
187    }
188
189    /// Generate OpenAPI request body with schema for a specific proto method
190    fn generate_openapi_request_body_full(
191        &self,
192        proto_method: &crate::dynamic::proto_parser::ProtoMethod,
193        schemas: &mut serde_json::Map<String, serde_json::Value>,
194    ) -> serde_json::Value {
195        let schema_name = self.get_schema_name(&proto_method.input_type);
196        let input_descriptor_opt = None; // We would need dynamic descriptor pool to get this
197
198        // Create a basic schema reference for the input type
199        let schema_ref = if let Some(descriptor) = input_descriptor_opt {
200            schemas.insert(schema_name.clone(), self.generate_json_schema(&descriptor));
201            serde_json::json!({
202                "$ref": format!("#/components/schemas/{}", schema_name)
203            })
204        } else {
205            // Fallback schema when descriptor is not available
206            serde_json::json!({
207                "type": "object",
208                "description": format!("Protobuf message: {}", proto_method.input_type),
209                "additionalProperties": true
210            })
211        };
212
213        serde_json::json!({
214            "required": true,
215            "content": {
216                "application/json": {
217                    "schema": schema_ref,
218                    "example": self.generate_example_for_type(&proto_method.input_type)
219                }
220            }
221        })
222    }
223
224    /// Generate OpenAPI responses with schema for a specific proto method
225    fn generate_openapi_responses_full(
226        &self,
227        service_name: &str,
228        method_name: &str,
229        proto_method: &crate::dynamic::proto_parser::ProtoMethod,
230        schemas: &mut serde_json::Map<String, serde_json::Value>,
231    ) -> serde_json::Value {
232        let schema_name = self.get_schema_name(&proto_method.output_type);
233        let output_descriptor_opt = None; // We would need dynamic descriptor pool to get this
234
235        let success_schema = if let Some(descriptor) = output_descriptor_opt {
236            schemas.insert(schema_name.clone(), self.generate_json_schema(&descriptor));
237            serde_json::json!({
238                "$ref": format!("#/components/schemas/{}", schema_name)
239            })
240        } else {
241            // Fallback schema when descriptor is not available
242            serde_json::json!({
243                "type": "object",
244                "description": format!("Protobuf message: {}", proto_method.output_type),
245                "additionalProperties": true
246            })
247        };
248
249        let mut responses = serde_json::json!({
250            "200": {
251                "description": "Successful operation",
252                "content": {
253                    "application/json": {
254                        "schema": {
255                            "type": "object",
256                            "properties": {
257                                "success": {
258                                    "type": "boolean",
259                                    "description": "Whether the request was successful"
260                                },
261                                "data": success_schema,
262                                "error": {
263                                    "type": ["string", "null"],
264                                    "description": "Error message if success is false"
265                                },
266                                "metadata": {
267                                    "type": "object",
268                                    "description": "Additional metadata from gRPC response",
269                                    "additionalProperties": { "type": "string" }
270                                }
271                            },
272                            "required": ["success", "data", "error", "metadata"]
273                        },
274                        "example": {
275                            "success": true,
276                            "data": self.generate_example_for_type(&proto_method.output_type),
277                            "error": null,
278                            "metadata": {
279                                "x-mockforge-service": service_name,
280                                "x-mockforge-method": method_name
281                            }
282                        }
283                    }
284                }
285            },
286            "400": {
287                "description": "Bad request - invalid JSON or invalid parameters",
288                "content": {
289                    "application/json": {
290                        "schema": {
291                            "type": "object",
292                            "properties": {
293                                "success": { "type": "boolean" },
294                                "data": { "type": "null" },
295                                "error": { "type": "string" },
296                                "metadata": { "type": "object" }
297                            }
298                        },
299                        "example": {
300                            "success": false,
301                            "data": null,
302                            "error": "Invalid request format",
303                            "metadata": {}
304                        }
305                    }
306                }
307            },
308            "500": {
309                "description": "Internal server error",
310                "content": {
311                    "application/json": {
312                        "schema": {
313                            "type": "object",
314                            "properties": {
315                                "success": { "type": "boolean" },
316                                "data": { "type": "null" },
317                                "error": { "type": "string" },
318                                "metadata": { "type": "object" }
319                            }
320                        },
321                        "example": {
322                            "success": false,
323                            "data": null,
324                            "error": "Internal server error",
325                            "metadata": {}
326                        }
327                    }
328                }
329            }
330        });
331
332        // Add streaming response option for server streaming methods
333        if proto_method.server_streaming {
334            responses["200"]["content"]["text/event-stream"] = serde_json::json!({
335                "schema": {
336                    "type": "string",
337                    "description": "Server-sent events stream"
338                },
339                "example": "data: {\"message\":\"Stream started\"}\n\ndata: {\"message\":\"Hello World!\"}\n\ndata: {\"message\":\"Stream ended\"}\n\n"
340            });
341        }
342
343        responses
344    }
345
346    /// Generate schema name from protobuf type name
347    fn get_schema_name(&self, type_name: &str) -> String {
348        // Remove package prefix and clean up the name
349        let short_name = if let Some(dot_index) = type_name.rfind('.') {
350            &type_name[dot_index + 1..]
351        } else {
352            type_name
353        };
354
355        // Keep the name as PascalCase and append "Message" if not already present
356        let schema_name = short_name.to_string();
357
358        if !schema_name.ends_with("Message") {
359            format!("{}Message", schema_name)
360        } else {
361            schema_name
362        }
363    }
364
365    /// Generate example JSON for a protobuf type
366    fn generate_example_for_type(&self, type_name: &str) -> serde_json::Value {
367        // Simple example generation based on common protobuf messages
368        if type_name.contains("HelloRequest") {
369            serde_json::json!({
370                "name": "World",
371                "user_info": {
372                    "user_id": "12345"
373                }
374            })
375        } else if type_name.contains("HelloReply") {
376            serde_json::json!({
377                "message": "Hello World! This is a mock response from MockForge",
378                "timestamp": "2025-01-01T00:00:00Z"
379            })
380        } else {
381            serde_json::json!({
382                "example_field": "example_value"
383            })
384        }
385    }
386
387    /// Generate OpenAPI operation specification for a method
388    pub fn generate_openapi_operation(
389        &self,
390        service_name: &str,
391        method_name: &str,
392        method_descriptor: &prost_reflect::MethodDescriptor,
393    ) -> serde_json::Value {
394        let http_method = self.get_http_method(method_descriptor);
395
396        let mut operation = serde_json::json!({
397            "summary": format!("{} {}", method_name, service_name),
398            "description": format!("Calls the {} method on {} service", method_name, service_name),
399            "parameters": self.generate_openapi_parameters(),
400        });
401
402        // Add request body for methods that send data
403        if http_method != "get" {
404            operation["requestBody"] = self.generate_openapi_request_body(method_descriptor);
405        }
406
407        // Add responses
408        operation["responses"] = self.generate_openapi_responses(method_descriptor);
409
410        operation
411    }
412
413    /// Get appropriate HTTP method for gRPC method type
414    pub fn get_http_method(
415        &self,
416        method_descriptor: &prost_reflect::MethodDescriptor,
417    ) -> &'static str {
418        if method_descriptor.is_client_streaming() && method_descriptor.is_server_streaming() {
419            "post" // Bidirectional streaming uses POST
420        } else if method_descriptor.is_server_streaming() {
421            "get" // Server streaming uses GET (streaming response)
422        } else {
423            "post" // Unary and client streaming use POST
424        }
425    }
426
427    /// Generate OpenAPI parameters
428    fn generate_openapi_parameters(&self) -> serde_json::Value {
429        serde_json::json!([
430            {
431                "name": "stream",
432                "in": "query",
433                "description": "Streaming mode (none, server, client, bidirectional)",
434                "required": false,
435                "schema": {
436                    "type": "string",
437                    "enum": ["none", "server", "client", "bidirectional"]
438                }
439            }
440        ])
441    }
442
443    /// Generate OpenAPI request body specification
444    fn generate_openapi_request_body(
445        &self,
446        method_descriptor: &prost_reflect::MethodDescriptor,
447    ) -> serde_json::Value {
448        let input_descriptor = method_descriptor.input();
449
450        serde_json::json!({
451            "required": true,
452            "content": {
453                "application/json": {
454                    "schema": self.generate_json_schema(&input_descriptor)
455                }
456            }
457        })
458    }
459
460    /// Generate OpenAPI responses specification
461    fn generate_openapi_responses(
462        &self,
463        method_descriptor: &prost_reflect::MethodDescriptor,
464    ) -> serde_json::Value {
465        let output_descriptor = method_descriptor.output();
466        let success_schema = self.generate_json_schema(&output_descriptor);
467
468        let mut responses = serde_json::json!({
469            "200": {
470                "description": "Successful operation",
471                "content": {
472                    "application/json": {
473                        "schema": {
474                            "type": "object",
475                            "properties": {
476                                "success": {
477                                    "type": "boolean",
478                                    "description": "Whether the request was successful"
479                                },
480                                "data": success_schema,
481                                "error": {
482                                    "type": ["string", "null"],
483                                    "description": "Error message if success is false"
484                                },
485                                "metadata": {
486                                    "type": "object",
487                                    "description": "Additional metadata from gRPC response"
488                                }
489                            },
490                            "required": ["success", "data", "error", "metadata"]
491                        }
492                    }
493                }
494            },
495            "400": {
496                "description": "Bad request - invalid JSON or invalid parameters",
497                "content": {
498                    "application/json": {
499                        "schema": {
500                            "type": "object",
501                            "properties": {
502                                "success": { "type": "boolean" },
503                                "data": { "type": "null" },
504                                "error": { "type": "string" },
505                                "metadata": { "type": "object" }
506                            }
507                        }
508                    }
509                }
510            },
511            "500": {
512                "description": "Internal server error",
513                "content": {
514                    "application/json": {
515                        "schema": {
516                            "type": "object",
517                            "properties": {
518                                "success": { "type": "boolean" },
519                                "data": { "type": "null" },
520                                "error": { "type": "string" },
521                                "metadata": { "type": "object" }
522                            }
523                        }
524                    }
525                }
526            }
527        });
528
529        // For streaming methods, add streaming response option
530        if method_descriptor.is_server_streaming() {
531            responses["200"]["content"]["text/event-stream"] = serde_json::json!({
532                "schema": {
533                    "type": "string",
534                    "description": "Server-sent events stream"
535                }
536            });
537        }
538
539        responses
540    }
541
542    /// Generate JSON schema from protobuf message descriptor
543    pub fn generate_json_schema(
544        &self,
545        descriptor: &prost_reflect::MessageDescriptor,
546    ) -> serde_json::Value {
547        let mut properties = serde_json::Map::new();
548        let mut required = Vec::new();
549
550        for field in descriptor.fields() {
551            let field_name = field.name().to_string();
552            let field_schema = self.generate_field_schema(&field);
553
554            properties.insert(field_name.clone(), field_schema);
555
556            // Mark field as required if it's not optional
557            // Note: In proto3, all fields are effectively optional at the JSON level
558            // but we can mark them as required for better API documentation
559            if field.supports_presence() && !field.is_list() {
560                // For proto3, we can make educated guesses about required fields
561                // based on field name patterns
562                let field_name_lower = field.name().to_lowercase();
563                if !field_name_lower.contains("optional") && !field_name_lower.contains("_opt") {
564                    required.push(field_name);
565                }
566            }
567        }
568
569        let mut schema = serde_json::json!({
570            "type": "object",
571            "properties": properties
572        });
573
574        if !required.is_empty() {
575            schema["required"] = serde_json::Value::Array(
576                required.into_iter().map(serde_json::Value::String).collect(),
577            );
578        }
579
580        schema
581    }
582
583    /// Generate JSON schema for a single field
584    fn generate_field_schema(&self, field: &prost_reflect::FieldDescriptor) -> serde_json::Value {
585        let base_type = self.get_json_type_for_field(field);
586
587        if field.is_list() {
588            serde_json::json!({
589                "type": "array",
590                "items": base_type
591            })
592        } else {
593            base_type
594        }
595    }
596
597    /// Get JSON type for a protobuf field
598    fn get_json_type_for_field(&self, field: &prost_reflect::FieldDescriptor) -> serde_json::Value {
599        match field.kind() {
600            prost_reflect::Kind::Message(message_descriptor) => {
601                self.generate_json_schema(&message_descriptor)
602            }
603            prost_reflect::Kind::Enum(_) => {
604                serde_json::json!({
605                    "type": "string",
606                    "description": "Enum value as string"
607                })
608            }
609            prost_reflect::Kind::String => {
610                serde_json::json!({
611                    "type": "string"
612                })
613            }
614            prost_reflect::Kind::Int32
615            | prost_reflect::Kind::Sint32
616            | prost_reflect::Kind::Sfixed32 => {
617                serde_json::json!({
618                    "type": "integer",
619                    "format": "int32"
620                })
621            }
622            prost_reflect::Kind::Int64
623            | prost_reflect::Kind::Sint64
624            | prost_reflect::Kind::Sfixed64 => {
625                serde_json::json!({
626                    "type": "integer",
627                    "format": "int64"
628                })
629            }
630            prost_reflect::Kind::Uint32 | prost_reflect::Kind::Fixed32 => {
631                serde_json::json!({
632                    "type": "integer",
633                    "format": "uint32",
634                    "minimum": 0
635                })
636            }
637            prost_reflect::Kind::Uint64 | prost_reflect::Kind::Fixed64 => {
638                serde_json::json!({
639                    "type": "integer",
640                    "format": "uint64",
641                    "minimum": 0
642                })
643            }
644            prost_reflect::Kind::Float => {
645                serde_json::json!({
646                    "type": "number",
647                    "format": "float"
648                })
649            }
650            prost_reflect::Kind::Double => {
651                serde_json::json!({
652                    "type": "number",
653                    "format": "double"
654                })
655            }
656            prost_reflect::Kind::Bool => {
657                serde_json::json!({
658                    "type": "boolean"
659                })
660            }
661            prost_reflect::Kind::Bytes => {
662                serde_json::json!({
663                    "type": "string",
664                    "contentEncoding": "base64",
665                    "description": "Base64-encoded bytes"
666                })
667            }
668        }
669    }
670
671    /// Extract service name from route path
672    pub fn extract_service_name(&self, path: &str) -> Option<String> {
673        if path.starts_with(&self.config.base_path) {
674            let path_without_base = &path[self.config.base_path.len()..];
675            let parts: Vec<&str> = path_without_base.trim_start_matches('/').split('/').collect();
676
677            if parts.len() >= 2 && !parts[0].is_empty() && !parts[1].is_empty() {
678                Some(parts[0].to_string())
679            } else {
680                None
681            }
682        } else {
683            None
684        }
685    }
686
687    /// Extract method name from route path
688    pub fn extract_method_name(&self, path: &str) -> Option<String> {
689        if path.starts_with(&self.config.base_path) {
690            let path_without_base = &path[self.config.base_path.len()..];
691            let parts: Vec<&str> = path_without_base.trim_start_matches('/').split('/').collect();
692
693            if parts.len() >= 2 && !parts[0].is_empty() && !parts[1].is_empty() {
694                Some(parts[1].to_string())
695            } else {
696                None
697            }
698        } else {
699            None
700        }
701    }
702}
703
704#[cfg(test)]
705mod tests {
706    use super::*;
707
708    #[test]
709    fn test_clean_service_name() {
710        let config = HttpBridgeConfig::default();
711        let generator = RouteGenerator::new(config);
712
713        assert_eq!(generator.clean_service_name("mockforge.greeter.Greeter"), "greeter");
714        assert_eq!(generator.clean_service_name("MyService"), "myservice");
715        assert_eq!(generator.clean_service_name("My-Service_Name"), "my-service_name");
716    }
717
718    #[test]
719    fn test_clean_method_name() {
720        let config = HttpBridgeConfig::default();
721        let generator = RouteGenerator::new(config);
722
723        assert_eq!(generator.clean_method_name("SayHello"), "sayhello");
724        assert_eq!(generator.clean_method_name("Say_Hello"), "say_hello");
725        assert_eq!(generator.clean_method_name("GetUserData"), "getuserdata");
726    }
727
728    #[test]
729    fn test_generate_route_path() {
730        let config = HttpBridgeConfig {
731            base_path: "/api".to_string(),
732            ..Default::default()
733        };
734        let generator = RouteGenerator::new(config);
735
736        let path = generator.generate_route_path("mockforge.greeter.Greeter", "SayHello");
737        assert_eq!(path, "/api/greeter/sayhello");
738        // Note: Service name is cleaned (package prefix removed, lowercased)
739        // Method name is cleaned (lowercased)
740    }
741
742    #[test]
743    fn test_extract_service_name() {
744        let config = HttpBridgeConfig {
745            base_path: "/api".to_string(),
746            ..Default::default()
747        };
748        let generator = RouteGenerator::new(config);
749
750        assert_eq!(
751            generator.extract_service_name("/api/greeter/sayhello"),
752            Some("greeter".to_string())
753        );
754        assert_eq!(generator.extract_service_name("/api/user/get"), Some("user".to_string()));
755        assert_eq!(generator.extract_service_name("/other/path"), None);
756    }
757
758    #[test]
759    fn test_extract_method_name() {
760        let config = HttpBridgeConfig {
761            base_path: "/api".to_string(),
762            ..Default::default()
763        };
764        let generator = RouteGenerator::new(config);
765
766        assert_eq!(
767            generator.extract_method_name("/api/greeter/sayhello"),
768            Some("sayhello".to_string())
769        );
770        assert_eq!(generator.extract_method_name("/api/user/get"), Some("get".to_string()));
771        assert_eq!(generator.extract_method_name("/api/single"), None);
772    }
773
774    #[test]
775    fn test_route_generator_creation() {
776        let config = HttpBridgeConfig {
777            enabled: true,
778            base_path: "/api".to_string(),
779            enable_cors: true,
780            max_request_size: 1024,
781            timeout_seconds: 30,
782            route_pattern: "/{service}/{method}".to_string(),
783        };
784
785        let generator = RouteGenerator::new(config.clone());
786        assert_eq!(generator.config.base_path, "/api");
787        assert_eq!(generator.config.route_pattern, "/{service}/{method}");
788    }
789
790    #[test]
791    fn test_clean_service_name_comprehensive() {
792        let config = HttpBridgeConfig::default();
793        let generator = RouteGenerator::new(config);
794
795        // Test various service name patterns
796        let test_cases = vec![
797            ("simple.Service", "service"),
798            ("com.example.MyService", "myservice"),
799            ("org.test.API", "api"),
800            ("Service", "service"),
801            ("ServiceName", "servicename"),
802            ("service-name", "service-name"),
803            ("service_name", "service_name"),
804            ("service.name", "name"),
805            ("a.b.c.d.Service", "service"),
806            ("Service123", "service123"),
807            ("123Service", "123service"),
808            ("Service-123", "service-123"),
809            ("Service_123", "service_123"),
810            ("Service.Name", "name"),
811        ];
812
813        for (input, expected) in test_cases {
814            assert_eq!(
815                generator.clean_service_name(input),
816                expected,
817                "Failed for input: {}",
818                input
819            );
820        }
821    }
822
823    #[test]
824    fn test_clean_method_name_comprehensive() {
825        let config = HttpBridgeConfig::default();
826        let generator = RouteGenerator::new(config);
827
828        // Test various method name patterns
829        let test_cases = vec![
830            ("GetUser", "getuser"),
831            ("getUser", "getuser"),
832            ("Get_User", "get_user"),
833            ("GetUserData", "getuserdata"),
834            ("getUserData", "getuserdata"),
835            ("GetUser_Data", "getuser_data"),
836            ("method123", "method123"),
837            ("123method", "123method"),
838            ("Method-123", "method-123"),
839            ("Method_123", "method_123"),
840            ("MethodName", "methodname"),
841            ("method_Name", "method_name"),
842            ("method-name", "method-name"),
843            ("method.name", "method.name"),
844        ];
845
846        for (input, expected) in test_cases {
847            assert_eq!(generator.clean_method_name(input), expected, "Failed for input: {}", input);
848        }
849    }
850
851    #[test]
852    fn test_generate_route_path_comprehensive() {
853        let test_configs = vec![
854            ("/api", "/{service}/{method}"),
855            ("/v1", "/{service}/{method}"),
856            ("/api/v1", "/{service}/{method}"),
857            ("/bridge", "/{service}/{method}"),
858            ("", "/{service}/{method}"),
859        ];
860
861        for (base_path, route_pattern) in test_configs {
862            let config = HttpBridgeConfig {
863                base_path: base_path.to_string(),
864                route_pattern: route_pattern.to_string(),
865                ..Default::default()
866            };
867            let generator = RouteGenerator::new(config);
868
869            let test_cases = vec![
870                ("com.example.Greeter", "SayHello"),
871                ("Service", "Method"),
872                ("test.Service", "testMethod"),
873                ("org.example.v1.UserService", "GetUser"),
874            ];
875
876            for (service, method) in test_cases {
877                let path = generator.generate_route_path(service, method);
878                let expected = format!("{}{}", base_path, route_pattern)
879                    .replace("{service}", &generator.clean_service_name(service))
880                    .replace("{method}", &generator.clean_method_name(method));
881
882                assert_eq!(path, expected, "Failed for service: {}, method: {}", service, method);
883            }
884        }
885    }
886
887    #[test]
888    fn test_generate_url_pattern_comprehensive() {
889        let config = HttpBridgeConfig {
890            base_path: "/api".to_string(),
891            route_pattern: "/{service}/{method}".to_string(),
892            ..Default::default()
893        };
894        let generator = RouteGenerator::new(config);
895
896        let test_cases = vec![
897            ("com.example.Greeter", "SayHello"),
898            ("Service", "Method"),
899            ("test.Service", "testMethod"),
900        ];
901
902        for (service, method) in test_cases {
903            let pattern = generator.generate_url_pattern(service, method);
904            assert!(pattern.starts_with("/api/"), "Pattern should start with /api/: {}", pattern);
905            assert!(
906                pattern.contains("[^/]+"),
907                "Pattern should contain regex for service: {}",
908                pattern
909            );
910            assert!(
911                pattern.contains("[^/]+"),
912                "Pattern should contain regex for method: {}",
913                pattern
914            );
915        }
916    }
917
918    #[test]
919    fn test_extract_service_name_comprehensive() {
920        // Test with /api base path
921        let config = HttpBridgeConfig {
922            base_path: "/api".to_string(),
923            route_pattern: "/{service}/{method}".to_string(),
924            ..Default::default()
925        };
926        let generator = RouteGenerator::new(config);
927
928        let test_paths = vec![
929            ("/api/greeter/sayhello", Some("greeter".to_string())),
930            ("/api/user/get", Some("user".to_string())),
931            ("/api/complex.service/name", Some("complex.service".to_string())),
932            ("/api/single", None),     // Not enough parts
933            ("/v1/test/method", None), // Wrong base path
934            ("/other/path", None),     // Wrong base path
935            ("", None),
936            ("/api/", None),
937            ("/api/greeter/", None), // Empty method name
938        ];
939
940        for (path, expected) in test_paths {
941            let result = generator.extract_service_name(path);
942            assert_eq!(result, expected, "Failed for path: {} with base /api", path);
943        }
944
945        // Test with /v1 base path
946        let config = HttpBridgeConfig {
947            base_path: "/v1".to_string(),
948            route_pattern: "/{service}/{method}".to_string(),
949            ..Default::default()
950        };
951        let generator = RouteGenerator::new(config);
952
953        let test_paths = vec![
954            ("/v1/test/method", Some("test".to_string())),
955            ("/v1/service/action", Some("service".to_string())),
956            ("/api/greeter/sayhello", None), // Wrong base path
957        ];
958
959        for (path, expected) in test_paths {
960            let result = generator.extract_service_name(path);
961            assert_eq!(result, expected, "Failed for path: {} with base /v1", path);
962        }
963    }
964
965    #[test]
966    fn test_extract_method_name_comprehensive() {
967        // Test with /api base path
968        let config = HttpBridgeConfig {
969            base_path: "/api".to_string(),
970            route_pattern: "/{service}/{method}".to_string(),
971            ..Default::default()
972        };
973        let generator = RouteGenerator::new(config);
974
975        let test_paths = vec![
976            ("/api/greeter/sayhello", Some("sayhello".to_string())),
977            ("/api/user/get", Some("get".to_string())),
978            ("/api/complex.service/method_name", Some("method_name".to_string())),
979            ("/api/single", None),     // Not enough parts
980            ("/v1/test/method", None), // Wrong base path
981            ("/other/path", None),     // Wrong base path
982            ("", None),
983            ("/api/", None),
984            ("/api/greeter/", None), // Empty method name
985        ];
986
987        for (path, expected) in test_paths {
988            let result = generator.extract_method_name(path);
989            assert_eq!(result, expected, "Failed for path: {} with base /api", path);
990        }
991
992        // Test with /v1 base path
993        let config = HttpBridgeConfig {
994            base_path: "/v1".to_string(),
995            route_pattern: "/{service}/{method}".to_string(),
996            ..Default::default()
997        };
998        let generator = RouteGenerator::new(config);
999
1000        let test_paths = vec![
1001            ("/v1/test/method", Some("method".to_string())),
1002            ("/v1/service/action", Some("action".to_string())),
1003            ("/api/greeter/sayhello", None), // Wrong base path
1004        ];
1005
1006        for (path, expected) in test_paths {
1007            let result = generator.extract_method_name(path);
1008            assert_eq!(result, expected, "Failed for path: {} with base /v1", path);
1009        }
1010    }
1011
1012    #[test]
1013    fn test_get_http_method() {
1014        let config = HttpBridgeConfig::default();
1015        let _generator = RouteGenerator::new(config);
1016
1017        // Create mock method descriptors for different streaming types
1018        // Note: This is simplified since we don't have actual descriptors
1019
1020        // Test different streaming combinations
1021        // For now, we'll test the logic with simple boolean combinations
1022        // In a real test, we'd need actual MethodDescriptor instances
1023
1024        // Since we can't easily create MethodDescriptor instances in this test,
1025        // we'll test the service name and method name cleaning functions
1026        // which are the core functionality we can test without actual descriptors
1027    }
1028
1029    #[test]
1030    fn test_generate_openapi_parameters() {
1031        let config = HttpBridgeConfig::default();
1032        let generator = RouteGenerator::new(config);
1033
1034        let params = generator.generate_openapi_parameters();
1035        assert!(params.is_array(), "Parameters should be an array");
1036
1037        if let serde_json::Value::Array(params_array) = params {
1038            assert!(!params_array.is_empty(), "Parameters array should not be empty");
1039
1040            // Check if stream parameter exists
1041            let stream_param = params_array
1042                .iter()
1043                .find(|p| p.get("name").and_then(|n| n.as_str()) == Some("stream"));
1044
1045            assert!(stream_param.is_some(), "Stream parameter should exist");
1046        }
1047    }
1048
1049    #[test]
1050    fn test_get_schema_name() {
1051        let config = HttpBridgeConfig::default();
1052        let generator = RouteGenerator::new(config);
1053
1054        let test_cases = vec![
1055            ("com.example.GetUserRequest", "GetUserRequestMessage"),
1056            ("GetUserRequest", "GetUserRequestMessage"),
1057            ("Request", "RequestMessage"),
1058            ("Response", "ResponseMessage"),
1059            ("Message", "Message"),
1060            ("com.example.v1.GetUserRequest", "GetUserRequestMessage"),
1061            ("org.test.APIRequest", "APIRequestMessage"),
1062        ];
1063
1064        for (input, expected) in test_cases {
1065            let result = generator.get_schema_name(input);
1066            assert_eq!(result, expected, "Failed for input: {}", input);
1067        }
1068    }
1069
1070    #[test]
1071    fn test_generate_example_for_type() {
1072        let config = HttpBridgeConfig::default();
1073        let generator = RouteGenerator::new(config);
1074
1075        // Test various type names to ensure example generation works
1076        let test_types = vec![
1077            "HelloRequest",
1078            "HelloReply",
1079            "GetUserRequest",
1080            "GetUserResponse",
1081            "UnknownMessage",
1082            "TestMessage",
1083            "com.example.TestMessage",
1084        ];
1085
1086        for type_name in test_types {
1087            let example = generator.generate_example_for_type(type_name);
1088            assert!(example.is_object(), "Example should be an object for type: {}", type_name);
1089            assert!(
1090                !example.as_object().unwrap().is_empty(),
1091                "Example object should not be empty for type: {}",
1092                type_name
1093            );
1094        }
1095    }
1096
1097    #[test]
1098    fn test_regex_patterns() {
1099        let config = HttpBridgeConfig::default();
1100        let generator = RouteGenerator::new(config);
1101
1102        // Test service name regex (should be lowercased)
1103        let service_test_cases = vec![
1104            ("MyService", "myservice"),
1105            ("My-Service", "my-service"),
1106            ("My_Service", "my_service"),
1107            ("My123Service", "my123service"),
1108            ("My.Service", "service"),
1109            ("My@Service", "my-service"),
1110            ("My#Service", "my-service"),
1111            ("My$Service", "my-service"),
1112        ];
1113
1114        for (input, expected) in service_test_cases {
1115            let cleaned = generator.clean_service_name(input);
1116            assert_eq!(cleaned, expected, "Service name regex failed for: {}", input);
1117        }
1118
1119        // Test method name regex (should be lowercased)
1120        let method_test_cases = vec![
1121            ("GetUser", "getuser"),
1122            ("Get-User", "get-user"),
1123            ("Get_User", "get_user"),
1124            ("Get123User", "get123user"),
1125            ("Get.User", "get.user"),
1126            ("Get@User", "get-user"),
1127            ("Get#User", "get-user"),
1128            ("Get$User", "get-user"),
1129        ];
1130
1131        for (input, expected) in method_test_cases {
1132            let cleaned = generator.clean_method_name(input);
1133            assert_eq!(cleaned, expected, "Method name regex failed for: {}", input);
1134        }
1135    }
1136}