package api import ( "net/http" "strings" "github.com/gin-gonic/gin" "go.uber.org/zap" ) const ( // APIVersionHeader est le header HTTP pour spécifier la version de l'API APIVersionHeader = "X-API-Version" // AcceptHeaderVersion est le header Accept avec version (ex: application/vnd.veza.v1+json) AcceptHeaderVersion = "Accept" // DefaultAPIVersion est la version par défaut de l'API DefaultAPIVersion = "v1" // APIVersionKey est la clé utilisée pour stocker la version dans le contexte Gin APIVersionKey = "api_version" ) // APIVersion représente une version de l'API type APIVersion struct { Version string // Ex: "v1", "v2" Deprecated bool // Indique si la version est dépréciée SunsetDate string // Date de fin de support (RFC3339) Description string // Description de la version } // VersionManager gère les versions de l'API (BE-SVC-019) type VersionManager struct { versions map[string]*APIVersion defaultVersion string logger *zap.Logger } // NewVersionManager crée un nouveau gestionnaire de versions func NewVersionManager(logger *zap.Logger) *VersionManager { vm := &VersionManager{ versions: make(map[string]*APIVersion), defaultVersion: DefaultAPIVersion, logger: logger, } // Enregistrer les versions par défaut vm.RegisterVersion(&APIVersion{ Version: "v1", Deprecated: false, Description: "Current stable API version", }) return vm } // RegisterVersion enregistre une nouvelle version de l'API func (vm *VersionManager) RegisterVersion(version *APIVersion) { vm.versions[version.Version] = version vm.logger.Info("API version registered", zap.String("version", version.Version), zap.Bool("deprecated", version.Deprecated), zap.String("description", version.Description)) } // GetVersion retourne les informations sur une version func (vm *VersionManager) GetVersion(version string) (*APIVersion, bool) { v, exists := vm.versions[version] return v, exists } // GetDefaultVersion retourne la version par défaut func (vm *VersionManager) GetDefaultVersion() string { return vm.defaultVersion } // SetDefaultVersion définit la version par défaut func (vm *VersionManager) SetDefaultVersion(version string) { if _, exists := vm.versions[version]; exists { vm.defaultVersion = version } } // GetAllVersions retourne toutes les versions disponibles func (vm *VersionManager) GetAllVersions() map[string]*APIVersion { result := make(map[string]*APIVersion) for k, v := range vm.versions { result[k] = v } return result } // VersionMiddleware est un middleware pour gérer le versioning de l'API (BE-SVC-019) // Supporte plusieurs méthodes de spécification de version : // 1. Header X-API-Version // 2. Header Accept: application/vnd.veza.v1+json // 3. URL path: /api/v1/... func VersionMiddleware(versionManager *VersionManager) gin.HandlerFunc { return func(c *gin.Context) { // Extraire la version depuis différentes sources version := extractAPIVersion(c) // Valider la version if version == "" { version = versionManager.GetDefaultVersion() } // Vérifier si la version existe apiVersion, exists := versionManager.GetVersion(version) if !exists { c.JSON(http.StatusBadRequest, gin.H{ "error": "Unsupported API version", "version": version, "available_versions": getAvailableVersions(versionManager), }) c.Abort() return } // Stocker la version dans le contexte c.Set(APIVersionKey, version) c.Set("api_version_info", apiVersion) // Ajouter des headers de réponse pour indiquer la version utilisée c.Header(APIVersionHeader, version) if apiVersion.Deprecated { c.Header("X-API-Deprecated", "true") c.Header("X-API-Version-Deprecated", "true") if apiVersion.SunsetDate != "" { c.Header("Sunset", apiVersion.SunsetDate) } } // Logger si version dépréciée if apiVersion.Deprecated { versionManager.logger.Warn("Deprecated API version used", zap.String("version", version), zap.String("path", c.Request.URL.Path), zap.String("sunset_date", apiVersion.SunsetDate)) } c.Next() } } // extractAPIVersion extrait la version de l'API depuis différentes sources func extractAPIVersion(c *gin.Context) string { // 1. Header X-API-Version if version := c.GetHeader(APIVersionHeader); version != "" { return normalizeVersion(version) } // 2. Header Accept: application/vnd.veza.v1+json if accept := c.GetHeader(AcceptHeaderVersion); accept != "" { if version := parseAcceptHeader(accept); version != "" { return version } } // 3. URL path: /api/v1/... ou /api/v2/... path := c.Request.URL.Path if strings.HasPrefix(path, "/api/") { parts := strings.Split(path, "/") if len(parts) >= 3 && strings.HasPrefix(parts[2], "v") { return normalizeVersion(parts[2]) } } return "" } // normalizeVersion normalise une version (v1 -> v1, 1 -> v1, etc.) func normalizeVersion(version string) string { version = strings.TrimSpace(version) version = strings.ToLower(version) if !strings.HasPrefix(version, "v") { version = "v" + version } return version } // parseAcceptHeader parse le header Accept pour extraire la version // Format: application/vnd.veza.v1+json func parseAcceptHeader(accept string) string { parts := strings.Split(accept, ",") for _, part := range parts { part = strings.TrimSpace(part) // Chercher le pattern vnd.veza.vX if strings.Contains(part, "vnd.veza.v") { start := strings.Index(part, "vnd.veza.v") if start != -1 { start += len("vnd.veza.v") end := start for end < len(part) && (part[end] >= '0' && part[end] <= '9') { end++ } if end > start { return normalizeVersion("v" + part[start:end]) } } } } return "" } // getAvailableVersions retourne la liste des versions disponibles func getAvailableVersions(vm *VersionManager) []string { versions := make([]string, 0, len(vm.versions)) for v := range vm.versions { versions = append(versions, v) } return versions } // GetAPIVersion retourne la version de l'API depuis le contexte Gin func GetAPIVersion(c *gin.Context) string { if version, exists := c.Get(APIVersionKey); exists { if v, ok := version.(string); ok { return v } } return DefaultAPIVersion } // GetAPIVersionInfo retourne les informations complètes sur la version depuis le contexte func GetAPIVersionInfo(c *gin.Context) *APIVersion { if info, exists := c.Get("api_version_info"); exists { if apiVersion, ok := info.(*APIVersion); ok { return apiVersion } } return nil } // VersionInfoHandler retourne les informations sur les versions disponibles func VersionInfoHandler(versionManager *VersionManager) gin.HandlerFunc { return func(c *gin.Context) { versions := versionManager.GetAllVersions() response := gin.H{ "current_version": versionManager.GetDefaultVersion(), "versions": make(map[string]interface{}), } for version, info := range versions { versionInfo := gin.H{ "version": info.Version, "deprecated": info.Deprecated, "description": info.Description, } if info.SunsetDate != "" { versionInfo["sunset_date"] = info.SunsetDate } response["versions"].(map[string]interface{})[version] = versionInfo } c.JSON(http.StatusOK, response) } }