// Package gateway provides API Gateway implementation. package gateway import ( "net/http" "strings" "git.dcentral.systems/toolz/goplt/internal/client" "git.dcentral.systems/toolz/goplt/pkg/config" "git.dcentral.systems/toolz/goplt/pkg/logger" "git.dcentral.systems/toolz/goplt/pkg/registry" "github.com/gin-gonic/gin" ) // Gateway handles routing requests to backend services. type Gateway struct { config config.ConfigProvider log logger.Logger clientFactory *client.ServiceClientFactory registry registry.ServiceRegistry routes []RouteConfig } // RouteConfig defines a route configuration. type RouteConfig struct { Path string Service string AuthRequired bool } // NewGateway creates a new API Gateway instance. func NewGateway( cfg config.ConfigProvider, log logger.Logger, clientFactory *client.ServiceClientFactory, reg registry.ServiceRegistry, ) (*Gateway, error) { // Load route configurations routes := loadRoutes(cfg) return &Gateway{ config: cfg, log: log, clientFactory: clientFactory, registry: reg, routes: routes, }, nil } // SetupRoutes configures routes on the Gin router. func (g *Gateway) SetupRoutes(router *gin.Engine) { // Register routes with wildcard support // Gin uses /*path for wildcards, so we convert /** to /*path for _, route := range g.routes { route := route // Capture for closure // Convert /** wildcard to Gin's /*path format ginPath := route.Path hasWildcard := strings.HasSuffix(ginPath, "/**") if hasWildcard { ginPath = strings.TrimSuffix(ginPath, "/**") + "/*path" } // Register all HTTP methods for this route router.Any(ginPath, func(c *gin.Context) { // Extract the remaining path var remainingPath string if hasWildcard { // Extract from Gin's path parameter pathParam := c.Param("path") if pathParam != "" { remainingPath = "/" + pathParam } else { remainingPath = "/" } } else { // Exact match - no remaining path remainingPath = "/" } // Route to appropriate service handler switch route.Service { case "auth-service": g.handleAuthService(c, route, remainingPath) case "identity-service": g.handleIdentityService(c, route, remainingPath) default: g.log.Warn("Unknown service", logger.String("service", route.Service), ) c.JSON(http.StatusNotFound, gin.H{ "error": "Service not found", }) } }) } // Default handler for unmatched routes router.NoRoute(func(c *gin.Context) { c.JSON(http.StatusNotFound, gin.H{ "error": "Route not found", "path": c.Request.URL.Path, }) }) } // matchRoute finds the matching route configuration for a given path. func (g *Gateway) matchRoute(path string) *RouteConfig { for _, route := range g.routes { if g.pathMatches(path, route.Path) { return &route } } return nil } // pathMatches checks if a request path matches a route pattern. // Supports wildcard matching: "/api/v1/auth/**" matches "/api/v1/auth/login", etc. func (g *Gateway) pathMatches(requestPath, routePath string) bool { // Remove trailing slashes for comparison requestPath = strings.TrimSuffix(requestPath, "/") routePath = strings.TrimSuffix(routePath, "/") // Exact match if requestPath == routePath { return true } // Wildcard match: routePath ends with "/**" if strings.HasSuffix(routePath, "/**") { prefix := strings.TrimSuffix(routePath, "/**") return strings.HasPrefix(requestPath, prefix+"/") || requestPath == prefix } return false } // extractRemainingPath extracts the path segment after the route prefix. // Example: path="/api/v1/auth/login", route="/api/v1/auth/**" -> returns "/login" func (g *Gateway) extractRemainingPath(requestPath, routePath string) string { // Remove trailing slashes requestPath = strings.TrimSuffix(requestPath, "/") routePath = strings.TrimSuffix(routePath, "/") // Handle wildcard routes if strings.HasSuffix(routePath, "/**") { prefix := strings.TrimSuffix(routePath, "/**") if strings.HasPrefix(requestPath, prefix) { remaining := strings.TrimPrefix(requestPath, prefix) if remaining == "" { return "/" } return remaining } } // Exact match - no remaining path if requestPath == routePath { return "/" } return "" } // loadRoutes loads route configurations from config. func loadRoutes(cfg config.ConfigProvider) []RouteConfig { if cfg == nil { return []RouteConfig{} } // Get routes from config routesInterface := cfg.Get("gateway.routes") if routesInterface == nil { return []RouteConfig{} } // Convert to slice of RouteConfig routesSlice, ok := routesInterface.([]interface{}) if !ok { return []RouteConfig{} } routes := make([]RouteConfig, 0, len(routesSlice)) for _, routeInterface := range routesSlice { routeMap, ok := routeInterface.(map[string]interface{}) if !ok { continue } route := RouteConfig{} if path, ok := routeMap["path"].(string); ok { route.Path = path } if service, ok := routeMap["service"].(string); ok { route.Service = service } if authRequired, ok := routeMap["auth_required"].(bool); ok { route.AuthRequired = authRequired } // Only add route if it has required fields if route.Path != "" && route.Service != "" { routes = append(routes, route) } } return routes }