feat: Add cache cmd

Implements: https://todo.sr.ht/~timharek/yr/4
Signed-off-by: Tim Hårek Andreassen <tim@harek.no>
This commit is contained in:
Tim Hårek Andreassen 2024-10-17 21:16:39 +02:00
parent 04059d8af6
commit 985f8a53d3
No known key found for this signature in database
GPG key ID: E59C7734F0E10EB5
4 changed files with 155 additions and 10 deletions

64
cmd/cache.go Normal file
View file

@ -0,0 +1,64 @@
package cmd
import (
"fmt"
"os"
"git.sr.ht/~timharek/yr/yr"
"github.com/spf13/cobra"
)
var cacheCmd = &cobra.Command{
Use: "cache",
Short: "Get details about your cached locations",
Args: cobra.MaximumNArgs(0),
Run: caches,
}
var cacheClearCmd = &cobra.Command{
Use: "clear",
Short: "Clear cache",
Args: cobra.MaximumNArgs(0),
Run: cachesClear,
}
func init() {
rootCmd.AddCommand(cacheCmd)
cacheCmd.AddCommand(cacheClearCmd)
}
func caches(cmd *cobra.Command, args []string) {
c, err := yr.New()
cobra.CheckErr(err)
list, err := c.Cache.List()
cobra.CheckErr(err)
if len(list) == 0 {
fmt.Println("No location is cached")
os.Exit(0)
}
// NOTE: Both location name and coordinates are used to store files
fmt.Println("Cached locations")
for _, item := range list {
fmt.Println("\t" + item.Filename)
}
}
func cachesClear(cmd *cobra.Command, args []string) {
c, err := yr.New()
cobra.CheckErr(err)
list, err := c.Cache.List()
cobra.CheckErr(err)
if len(list) == 0 {
fmt.Println("No location is cached")
os.Exit(0)
}
err = c.Cache.Clear()
cobra.CheckErr(err)
fmt.Printf("Cleared %d locations from cache\n", len(list))
}

72
pkg/cache/cache.go vendored
View file

@ -7,6 +7,7 @@ import (
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"time" "time"
) )
@ -33,7 +34,7 @@ func (c *Cache[T]) Add(name string, data any) error {
return fmt.Errorf("Failed to ensure cache-dir is present.") return fmt.Errorf("Failed to ensure cache-dir is present.")
} }
filePath := filepath.Join(c.Path, fmt.Sprintf("%s.json", name)) filePath := filepath.Join(c.Path, filename(name))
d := []byte(j) d := []byte(j)
err = os.WriteFile(filePath, d, 0646) err = os.WriteFile(filePath, d, 0646)
if err != nil { if err != nil {
@ -45,7 +46,7 @@ func (c *Cache[T]) Add(name string, data any) error {
// Get data from JSON-file from cache-dir, returns nil if no result // Get data from JSON-file from cache-dir, returns nil if no result
func (c *Cache[T]) Get(name string) (*T, error) { func (c *Cache[T]) Get(name string) (*T, error) {
filePath := filepath.Join(c.Path, fmt.Sprintf("%s.json", name)) filePath := filepath.Join(c.Path, filename(name))
if _, err := os.Stat(filePath); errors.Is(err, os.ErrNotExist) { if _, err := os.Stat(filePath); errors.Is(err, os.ErrNotExist) {
return nil, nil return nil, nil
} }
@ -70,6 +71,73 @@ func (c *Cache[T]) Get(name string) (*T, error) {
return &result, nil return &result, nil
} }
type Saved[T any] struct {
Filename string
Item T
}
func (c *Cache[T]) List() ([]Saved[T], error) {
var result []Saved[T]
filenames, err := list(c.Path)
if err != nil {
return nil, fmt.Errorf("Unable to read from cache path")
}
if len(filenames) == 0 {
return result, nil
}
for _, f := range filenames {
cached, err := c.Get(f)
if err != nil {
return nil, err
}
result = append(result, Saved[T]{Filename: f, Item: *(cached)})
}
return result, nil
}
func list(path string) ([]string, error) {
dir, err := os.ReadDir(path)
if err != nil {
return nil, fmt.Errorf("Unable to read from path")
}
var filenames []string
for _, item := range dir {
if !item.IsDir() && strings.Contains(item.Name(), prefix) {
tmp := strings.ReplaceAll(strings.ReplaceAll(item.Name(), ".json", ""), prefix, "")
filenames = append(filenames, tmp)
}
}
return filenames, nil
}
func (c *Cache[T]) Clear() error {
files, err := list(c.Path)
if err != nil {
return err
}
for _, f := range files {
filePath := filepath.Join(c.Path, filename(f))
err = os.Remove(filePath)
if err != nil {
return err
}
}
return nil
}
var prefix = "yr-"
func filename(name string) string {
return fmt.Sprintf("%s%s.json", prefix, name)
}
// Returns if t is expired // Returns if t is expired
func IsExpired(expire time.Time, now *time.Time) bool { func IsExpired(expire time.Time, now *time.Time) bool {
if now == nil { if now == nil {

View file

@ -17,14 +17,27 @@ func TestCache(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
c := New[testType]("") c := New[testType]("")
assert.Equal(os.TempDir(), c.Path) assert.Equal(os.TempDir(), c.Path)
err := c.Clear()
assert.NoError(err)
err := c.Add("something", &testType{Prop1: "test1", Prop2: "test2"}) err = c.Add("something", &testType{Prop1: "test1", Prop2: "test2"})
assert.NoError(err) assert.NoError(err)
result, err := c.Get("something") result, err := c.Get("something")
assert.NoError(err) assert.NoError(err)
assert.Equal("test1", result.Prop1) assert.Equal("test1", result.Prop1)
assert.Equal("test2", result.Prop2) assert.Equal("test2", result.Prop2)
list, err := c.List()
assert.Equal(1, len(list))
assert.NoError(err)
err = c.Add("something else", &testType{Prop1: "test1", Prop2: "test2"})
assert.NoError(err)
list, err = c.List()
assert.NoError(err)
assert.Equal(2, len(list))
} }
func TestIsExpired(t *testing.T) { func TestIsExpired(t *testing.T) {

View file

@ -15,7 +15,7 @@ import (
type Client struct { type Client struct {
met met.Met met met.Met
nom nominatim.Nominatim nom nominatim.Nominatim
cache cache.Cache[ForecastResult] Cache cache.Cache[ForecastResult]
} }
// New default Yr client // New default Yr client
@ -31,7 +31,7 @@ func New() (*Client, error) {
} }
cache := cache.New[ForecastResult]("") cache := cache.New[ForecastResult]("")
return &Client{met: *met, nom: *nom, cache: *cache}, nil return &Client{met: *met, nom: *nom, Cache: *cache}, nil
} }
type wind struct { type wind struct {
@ -89,7 +89,7 @@ type ForecastResult struct {
// Current forecast for q // Current forecast for q
func (c *Client) Now(q string) (*ForecastResult, error) { func (c *Client) Now(q string) (*ForecastResult, error) {
cacheResult, err := c.cache.Get(q) cacheResult, err := c.Cache.Get(q)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -122,7 +122,7 @@ func (c *Client) NowCoords(coords *nominatim.Coordinates, location *string) (*Fo
// Forecast for q // Forecast for q
func (c *Client) Forecast(q string) (*ForecastResult, error) { func (c *Client) Forecast(q string) (*ForecastResult, error) {
cacheResult, err := c.cache.Get(q) cacheResult, err := c.Cache.Get(q)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -144,7 +144,7 @@ func (c *Client) Forecast(q string) (*ForecastResult, error) {
// Forecast for coords // Forecast for coords
func (c *Client) ForecastCoords(coords *nominatim.Coordinates, location *string) (*ForecastResult, error) { func (c *Client) ForecastCoords(coords *nominatim.Coordinates, location *string) (*ForecastResult, error) {
coordCacheName := fmt.Sprintf("%.4f_%.4f", coords.Latitude, coords.Longitude) coordCacheName := fmt.Sprintf("%.4f_%.4f", coords.Latitude, coords.Longitude)
cacheResult, err := c.cache.Get(coordCacheName) cacheResult, err := c.Cache.Get(coordCacheName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -220,11 +220,11 @@ func (c *Client) ForecastCoords(coords *nominatim.Coordinates, location *string)
Forecast: forecasts, Forecast: forecasts,
} }
err = c.cache.Add(*location, result) err = c.Cache.Add(*location, result)
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = c.cache.Add(coordCacheName, result) err = c.Cache.Add(coordCacheName, result)
if err != nil { if err != nil {
return nil, err return nil, err
} }