diff --git a/cmd/forecast_helper.go b/cmd/forecast_helper.go index 9a496ca..bb4d105 100644 --- a/cmd/forecast_helper.go +++ b/cmd/forecast_helper.go @@ -13,15 +13,18 @@ import ( ) type forecastH struct { - isJson bool - isUTC bool - isWeb bool - f *yr.ForecastResult + isJson bool + isDebug bool + isUTC bool + isWeb bool + f *yr.ForecastResult } func forecastHelper(cmd *cobra.Command, args []string) *forecastH { isJson, err := cmd.Flags().GetBool(flags.JSON) cobra.CheckErr(err) + isDebug, err := cmd.Flags().GetBool(flags.DEBUG) + cobra.CheckErr(err) isUTC, err := cmd.Flags().GetBool(flags.UTC) cobra.CheckErr(err) isWeb, err := cmd.Flags().GetBool(flags.WEB) @@ -46,8 +49,15 @@ func forecastHelper(cmd *cobra.Command, args []string) *forecastH { cobra.CheckErr(err) } + if isDebug { + fmt.Printf("Expires: %v\n", f.Expires) + fmt.Printf("LastModified: %v\n", f.LastModified) + fmt.Printf("Default cache-dir: %v\n", os.TempDir()) + } + return &forecastH{ isJson, + isDebug, isUTC, isWeb, f, diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go index 0d341a4..0768e8c 100644 --- a/pkg/cache/cache.go +++ b/pkg/cache/cache.go @@ -7,6 +7,7 @@ import ( "io" "os" "path/filepath" + "time" ) type Cache[T any] struct { @@ -67,3 +68,13 @@ func (c *Cache[T]) Get(name string) (*T, error) { return &result, nil } + +// Returns if t is expired +func IsExpired(expire time.Time, now *time.Time) bool { + if now == nil { + tmp := time.Now() + now = &tmp + } + + return now.Unix() >= expire.Unix() +} diff --git a/pkg/cache/cache_test.go b/pkg/cache/cache_test.go index b9c1e77..491c82b 100644 --- a/pkg/cache/cache_test.go +++ b/pkg/cache/cache_test.go @@ -3,6 +3,7 @@ package cache import ( "os" "testing" + "time" "github.com/stretchr/testify/assert" ) @@ -25,3 +26,27 @@ func TestCache(t *testing.T) { assert.Equal("test1", result.Prop1) assert.Equal("test2", result.Prop2) } + +func TestIsExpired(t *testing.T) { + layout := "2006-01-02 15:04:05" + assert := assert.New(t) + expirationTime, err := time.Parse(layout, "2024-10-04 22:22:00") + assert.NoError(err) + now, err := time.Parse(layout, "2024-10-04 22:21:00") + assert.NoError(err) + + result := IsExpired(expirationTime, &now) + assert.False(result) + + now, err = time.Parse(layout, "2024-10-04 22:22:00") + assert.NoError(err) + + result = IsExpired(expirationTime, &now) + assert.True(result) + + now, err = time.Parse(layout, "2024-10-04 22:23:00") + assert.NoError(err) + + result = IsExpired(expirationTime, &now) + assert.True(result) +} diff --git a/pkg/met/met.go b/pkg/met/met.go index 7c14ea0..7122139 100644 --- a/pkg/met/met.go +++ b/pkg/met/met.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "net/http" + "time" ) type Met struct { @@ -53,11 +54,32 @@ func (m *Met) Forecast(lat, lon float64, alt *int) (*LocationForecastResult, err return nil, err } - forecast := LocationForecastResult{} + expiresRaw, ok := resp.Header["Expires"] + if !ok { + return nil, fmt.Errorf("Unable to get 'Expires' header") + } + expires, err := time.Parse(time.RFC1123, expiresRaw[0]) + if err != nil { + return nil, fmt.Errorf("Unable to parse `expiresRaw`: %w", err) + } + lastModifiedRaw, ok := resp.Header["Last-Modified"] + if !ok { + return nil, fmt.Errorf("Unable to get 'Last-Modified' header") + } + lastModified, err := time.Parse(time.RFC1123, lastModifiedRaw[0]) + if err != nil { + return nil, fmt.Errorf("Unable to parse `Last-Modified`: %w", err) + } + + forecast := LocationForecast{} err = json.Unmarshal(body, &forecast) if err != nil { return nil, err } - return &forecast, nil + return &LocationForecastResult{ + Expires: expires, + LastModified: lastModified, + LocationForecast: forecast, + }, nil } diff --git a/pkg/met/types.go b/pkg/met/types.go index c9f3433..754a610 100644 --- a/pkg/met/types.go +++ b/pkg/met/types.go @@ -2,12 +2,13 @@ package met import "time" -type cacheInfo struct { +type LocationForecastResult struct { Expires time.Time `json:"expires"` LastModified time.Time `json:"lastModified"` + LocationForecast } -type LocationForecastResult struct { +type LocationForecast struct { Type string `json:"type"` Geometry struct { diff --git a/yr/yr.go b/yr/yr.go index 589cca4..1267bb7 100644 --- a/yr/yr.go +++ b/yr/yr.go @@ -6,14 +6,16 @@ import ( "slices" "time" + "git.sr.ht/~timharek/yr/pkg/cache" "git.sr.ht/~timharek/yr/pkg/met" "git.sr.ht/~timharek/yr/pkg/nominatim" "git.sr.ht/~timharek/yr/yr/direction" ) type Client struct { - met met.Met - nom nominatim.Nominatim + met met.Met + nom nominatim.Nominatim + cache cache.Cache[ForecastResult] } func New() (*Client, error) { @@ -26,8 +28,9 @@ func New() (*Client, error) { if err != nil { return nil, err } + cache := cache.New[ForecastResult]("") - return &Client{met: *met, nom: *nom}, nil + return &Client{met: *met, nom: *nom, cache: *cache}, nil } type wind struct { @@ -74,11 +77,21 @@ type Forecast struct { } type ForecastResult struct { - Coordinates nominatim.Coordinates `json:"coordinates"` - Forecast []Forecast `json:"forecast"` + Expires time.Time `json:"expires"` + LastModified time.Time `json:"lastModified"` + Coordinates nominatim.Coordinates `json:"coordinates"` + Forecast []Forecast `json:"forecast"` } func (c *Client) Now(q string) (*ForecastResult, error) { + cacheResult, err := c.cache.Get(q) + if err != nil { + return nil, err + } + if cacheResult != nil && !cache.IsExpired(cacheResult.Expires, nil) { + return cacheResult, nil + } + coords, err := c.nom.Lookup(q) if err != nil { return nil, err @@ -102,6 +115,14 @@ func (c *Client) NowCoords(coords *nominatim.Coordinates, location *string) (*Fo } func (c *Client) Forecast(q string) (*ForecastResult, error) { + cacheResult, err := c.cache.Get(q) + if err != nil { + return nil, err + } + if cacheResult != nil && !cache.IsExpired(cacheResult.Expires, nil) { + return cacheResult, nil + } + coords, err := c.nom.Lookup(q) if err != nil { return nil, err @@ -114,6 +135,14 @@ func (c *Client) Forecast(q string) (*ForecastResult, error) { } func (c *Client) ForecastCoords(coords *nominatim.Coordinates, location *string) (*ForecastResult, error) { + coordCacheName := fmt.Sprintf("%.4f_%.4f", coords.Latitude, coords.Longitude) + cacheResult, err := c.cache.Get(coordCacheName) + if err != nil { + return nil, err + } + if cacheResult != nil && !cache.IsExpired(cacheResult.Expires, nil) { + return cacheResult, nil + } f, err := c.met.Forecast(coords.Latitude, coords.Longitude, nil) if err != nil { return nil, err @@ -173,13 +202,26 @@ func (c *Client) ForecastCoords(coords *nominatim.Coordinates, location *string) } - return &ForecastResult{ + result := ForecastResult{ + Expires: f.Expires, + LastModified: f.LastModified, Coordinates: nominatim.Coordinates{ Longitude: coords.Longitude, Latitude: coords.Latitude, }, Forecast: forecasts, - }, nil + } + + err = c.cache.Add(*location, result) + if err != nil { + return nil, err + } + err = c.cache.Add(coordCacheName, result) + if err != nil { + return nil, err + } + + return &result, nil } func sortTimeSeries(a, b met.Timeseries) int {