diff --git a/esi/cached.go b/esi/cached.go new file mode 100644 index 0000000..ff9281a --- /dev/null +++ b/esi/cached.go @@ -0,0 +1,128 @@ +package esi + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "time" + + logger "git.site.quack-lab.dev/dave/cylogger" +) + +// CacheEntry represents a cached API response +type CacheEntry struct { + ID uint `gorm:"primaryKey"` + URLHash string `gorm:"uniqueIndex"` + Response string `gorm:"type:text"` + CachedAt time.Time `gorm:"index"` +} + +// CachedESI implements ESIInterface with caching +type CachedESI struct { + direct ESIInterface + db interface { + GetCacheEntry(urlHash string) (*CacheEntry, error) + SaveCacheEntry(entry *CacheEntry) error + } +} + +// NewCachedESI creates a new CachedESI instance +func NewCachedESI(direct ESIInterface, db interface { + GetCacheEntry(urlHash string) (*CacheEntry, error) + SaveCacheEntry(entry *CacheEntry) error +}) *CachedESI { + return &CachedESI{ + direct: direct, + db: db, + } +} + +// GetCharacterPlanets retrieves a list of planets for a character with caching +func (c *CachedESI) GetCharacterPlanets(ctx context.Context, characterID int, accessToken string) ([]Planet, error) { + url := fmt.Sprintf("/v1/characters/%d/planets/", characterID) + result, err := func() (interface{}, error) { + var fetchFunc func() (interface{}, error) = func() (interface{}, error) { + return c.direct.GetCharacterPlanets(ctx, characterID, accessToken) + } + return c.getCachedResponse(url, fetchFunc) + }() + if err != nil { + return nil, err + } + return result.([]Planet), nil +} + +// GetPlanetDetails retrieves detailed information about a specific planet with caching +func (c *CachedESI) GetPlanetDetails(ctx context.Context, characterID, planetID int, accessToken string) (*PlanetDetail, error) { + url := fmt.Sprintf("/v3/characters/%d/planets/%d/", characterID, planetID) + result, err := func() (interface{}, error) { + var fetchFunc func() (interface{}, error) = func() (interface{}, error) { + return c.direct.GetPlanetDetails(ctx, characterID, planetID, accessToken) + } + return c.getCachedResponse(url, fetchFunc) + }() + if err != nil { + return nil, err + } + return result.(*PlanetDetail), nil +} + +// getCachedResponse handles caching logic +func (c *CachedESI) getCachedResponse(url string, fetchFunc func() (interface{}, error)) (interface{}, error) { + // Generate cache key + hash := sha256.Sum256([]byte(url)) + urlHash := hex.EncodeToString(hash[:]) + + // Check cache using the interface + cacheEntry, err := c.db.GetCacheEntry(urlHash) + if err == nil { + // Check if cache is still valid + cacheValidity, _ := time.ParseDuration("10m") // Default 10 minutes + if time.Since(cacheEntry.CachedAt) < cacheValidity { + logger.Debug("Cache hit for URL: %s", url) + // Parse cached response based on URL pattern + if url[len(url)-1:] == "/" { + // Planets endpoint + var planets []Planet + if err := json.Unmarshal([]byte(cacheEntry.Response), &planets); err == nil { + return planets, nil + } + } else { + // Planet details endpoint + var planetDetail PlanetDetail + if err := json.Unmarshal([]byte(cacheEntry.Response), &planetDetail); err == nil { + return &planetDetail, nil + } + } + } + } + + // Cache miss or invalid, fetch from API + logger.Debug("Cache miss for URL: %s", url) + result, err := fetchFunc() + if err != nil { + return nil, err + } + + // Store in cache + responseBytes, err := json.Marshal(result) + if err != nil { + logger.Warning("Failed to marshal response for caching: %v", err) + return result, nil + } + + cacheEntry = CacheEntry{ + URLHash: urlHash, + Response: string(responseBytes), + CachedAt: time.Now(), + } + + if err := c.db.SaveCacheEntry(&cacheEntry); err != nil { + logger.Warning("Failed to cache response: %v", err) + } + + logger.Debug("Cached response for URL: %s", url) + return result, nil +} diff --git a/esi/client.go b/esi/client.go index dd7812d..1769c45 100644 --- a/esi/client.go +++ b/esi/client.go @@ -92,8 +92,28 @@ type Route struct { Waypoints []int `json:"waypoints"` } +// ESIInterface defines the contract for ESI API interactions +type ESIInterface interface { + GetCharacterPlanets(ctx context.Context, characterID int, accessToken string) ([]Planet, error) + GetPlanetDetails(ctx context.Context, characterID, planetID int, accessToken string) (*PlanetDetail, error) +} + +// DirectESI implements ESIInterface with direct API calls +type DirectESI struct { + httpClient *http.Client +} + +// NewDirectESI creates a new DirectESI instance +func NewDirectESI() *DirectESI { + return &DirectESI{ + httpClient: &http.Client{ + Timeout: 30 * time.Second, + }, + } +} + // GetCharacterPlanets retrieves a list of planets for a character -func GetCharacterPlanets(ctx context.Context, characterID int, accessToken string) ([]Planet, error) { +func (d *DirectESI) GetCharacterPlanets(ctx context.Context, characterID int, accessToken string) ([]Planet, error) { logger.Debug("Fetching planets for character ID %d", characterID) url := fmt.Sprintf("%s/v1/characters/%d/planets/", ESIBaseURL, characterID) @@ -132,7 +152,7 @@ func GetCharacterPlanets(ctx context.Context, characterID int, accessToken strin } // GetPlanetDetails retrieves detailed information about a specific planet -func GetPlanetDetails(ctx context.Context, characterID, planetID int, accessToken string) (*PlanetDetail, error) { +func (d *DirectESI) GetPlanetDetails(ctx context.Context, characterID, planetID int, accessToken string) (*PlanetDetail, error) { logger.Debug("Fetching planet details for character ID %d, planet ID %d", characterID, planetID) url := fmt.Sprintf("%s/v3/characters/%d/planets/%d/", ESIBaseURL, characterID, planetID)