feat: add cache layer over api

This commit is contained in:
2026-02-08 21:26:39 +00:00
parent 3a18041c6f
commit 1eab9909e8
4 changed files with 133 additions and 1 deletions
+62
View File
@@ -0,0 +1,62 @@
package geodata
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
)
const cacheDuration = 30 * 24 * time.Hour
func getCachePath() string {
home, _ := os.UserHomeDir()
return filepath.Join(home, ".cache", "git-chronos", "geodata.json")
}
func loadFromCache() (TimezoneMap, error) {
path := getCachePath()
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
var cacheContainer cacheCountryData
if err := json.NewDecoder(file).Decode(&cacheContainer); err != nil {
return nil, err
}
if time.Since(cacheContainer.FetchedAt) > cacheDuration {
return nil, fmt.Errorf("cache expired")
}
return cacheContainer.Data, nil
}
func saveToCache(tzMap TimezoneMap) error {
path := getCachePath()
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 755); err != nil { // creates if not exists
return fmt.Errorf("failed to create cache dir")
}
file, err := os.Create(path) // create if not exists
if err != nil {
return err
}
defer file.Close()
container := cacheCountryData{
FetchedAt: time.Now(),
Data: tzMap,
}
encoder := json.NewEncoder(file)
encoder.SetIndent("", " ") // so it looks better
return encoder.Encode(container)
}
+21 -1
View File
@@ -8,7 +8,7 @@ import (
"time" "time"
) )
func FetchLive() (TimezoneMap, error) { func fetchLive() (TimezoneMap, error) {
client := http.Client{Timeout: 10 * time.Second} client := http.Client{Timeout: 10 * time.Second}
// from: https://restcountries.com // from: https://restcountries.com
@@ -32,6 +32,26 @@ func FetchLive() (TimezoneMap, error) {
return buildTzMap(apidData), nil return buildTzMap(apidData), nil
} }
func Fetch() (TimezoneMap, error) {
// try to get from cache
tzMap, err := loadFromCache()
if err == nil {
return tzMap, nil
}
// fetch live and save
tzMap, err = fetchLive()
if err != nil {
return nil, err
}
if err = saveToCache(tzMap); err != nil {
return nil, err
}
return tzMap, nil
}
func buildTzMap(data []countryApiReponse) TimezoneMap { func buildTzMap(data []countryApiReponse) TimezoneMap {
tzMap := make(TimezoneMap) tzMap := make(TimezoneMap)
+43
View File
@@ -0,0 +1,43 @@
package geodata
import (
"os"
"testing"
)
func TestFetchLive(t *testing.T) {
tzMap, err := fetchLive()
if err != nil {
t.Fatal(err)
}
testTzMap(tzMap, t)
}
func TestCache(t *testing.T) {
// delete cache
cachePath := getCachePath()
if err := os.Remove(cachePath); err != nil && !os.IsNotExist(err) {
t.Fatal(err)
}
_, err := Fetch() // this populates cache
if err != nil {
t.Fatal(err)
}
tzMap, err := loadFromCache()
if err != nil {
t.Fatal(err)
}
testTzMap(tzMap, t)
}
func testTzMap(tzMap TimezoneMap, t *testing.T) {
entry0100 := tzMap["+0100"]
if len(entry0100) == 0 {
t.Fatalf("no countries found to timezone")
}
}
+7
View File
@@ -1,5 +1,7 @@
package geodata package geodata
import "time"
// Country info exposed to other packages // Country info exposed to other packages
type Country struct { type Country struct {
Name string Name string
@@ -17,3 +19,8 @@ type countryApiReponse struct {
Population int64 `json:"population"` Population int64 `json:"population"`
Timezones []string `json:"timezones"` // example: [ "UTC+00:00" ] Timezones []string `json:"timezones"` // example: [ "UTC+00:00" ]
} }
type cacheCountryData struct {
FetchedAt time.Time `json:"fetched_at"`
Data TimezoneMap `json:"data"`
}