- Backend: Add X-API-Deprecated header alongside existing X-API-Version-Deprecated - Frontend: Show deprecation warning toast when deprecated API version detected - Warning shown only once per session to avoid spam - Includes sunset date in warning message if available
257 lines
7.2 KiB
Go
257 lines
7.2 KiB
Go
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)
|
|
}
|
|
}
|