Skip to content

Commit

Permalink
[filecache] Make it typed, add GetOrSet variants (#285)
Browse files Browse the repository at this point in the history
## Summary

Makes filecache better by:

* Make it typed
* export `Cache` type
* Add `GetOrSet` variants.

## How was it tested?
builds
  • Loading branch information
mikeland73 committed Apr 4, 2024
1 parent 580a516 commit 7b42192
Showing 1 changed file with 71 additions and 25 deletions.
96 changes: 71 additions & 25 deletions filecache/filecache.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,20 @@ import (
var NotFound = errors.New("not found")
var Expired = errors.New("expired")

type cache struct {
type Cache[T any] struct {
domain string
cacheDir string
}

type data struct {
Val []byte
type data[T any] struct {
Val T
Exp time.Time
}

type Option func(*cache)
type Option[T any] func(*Cache[T])

func New(domain string, opts ...Option) *cache {
result := &cache{domain: domain}
func New[T any](domain string, opts ...Option[T]) *Cache[T] {
result := &Cache[T]{domain: domain}

var err error
result.cacheDir, err = os.UserCacheDir()
Expand All @@ -41,56 +41,102 @@ func New(domain string, opts ...Option) *cache {
return result
}

func WithCacheDir(dir string) Option {
return func(c *cache) {
func WithCacheDir[T any](dir string) Option[T] {
return func(c *Cache[T]) {
c.cacheDir = dir
}
}

func (c *cache) Set(key string, val []byte, dur time.Duration) error {
d, err := json.Marshal(data{Val: val, Exp: time.Now().Add(dur)})
// Set stores a value in the cache with the given key and expiration duration.
func (c *Cache[T]) Set(key string, val T, dur time.Duration) error {
d, err := json.Marshal(data[T]{Val: val, Exp: time.Now().Add(dur)})
if err != nil {
return errors.WithStack(err)
}

return errors.WithStack(os.WriteFile(c.filename(key), d, 0644))
}

func (c *cache) SetT(key string, val []byte, t time.Time) error {
d, err := json.Marshal(data{Val: val, Exp: t})
// SetWithTime is like Set but it allows the caller to specify the expiration
// time of the value.
func (c *Cache[T]) SetWithTime(key string, val T, t time.Time) error {
d, err := json.Marshal(data[T]{Val: val, Exp: t})
if err != nil {
return errors.WithStack(err)
}

return errors.WithStack(os.WriteFile(c.filename(key), d, 0644))
}

func (c *cache) Get(key string) ([]byte, error) {
// Get retrieves a value from the cache with the given key.
func (c *Cache[T]) Get(key string) (T, error) {
path := c.filename(key)
resultData := data[T]{}

if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
return nil, NotFound
return resultData.Val, NotFound
}

content, err := os.ReadFile(path)
if err != nil {
return nil, errors.WithStack(err)
return resultData.Val, errors.WithStack(err)
}
d := data{}
if err := json.Unmarshal(content, &d); err != nil {
return nil, errors.WithStack(err)

if err := json.Unmarshal(content, &resultData); err != nil {
return resultData.Val, errors.WithStack(err)
}
if time.Now().After(d.Exp) {
return nil, Expired
if time.Now().After(resultData.Exp) {
return resultData.Val, Expired
}
return d.Val, nil
return resultData.Val, nil
}

func (c *cache) filename(key string) string {
dir := filepath.Join(c.cacheDir, c.domain)
_ = os.MkdirAll(dir, 0755)
return filepath.Join(dir, key)
// GetOrSet is a convenience method that gets the value from the cache if it
// exists, otherwise it calls the provided function to get the value and sets
// it in the cache.
// If the function returns an error, the error is returned and the value is not
// cached.
func (c *Cache[T]) GetOrSet(
key string,
f func() (T, time.Duration, error),
) (T, error) {
if val, err := c.Get(key); err == nil || !IsCacheMiss(err) {
return val, err
}

val, dur, err := f()
if err != nil {
return val, err
}

return val, c.Set(key, val, dur)
}

// GetOrSetWithTime is like GetOrSet but it allows the caller to specify the
// expiration time of the value.
func (c *Cache[T]) GetOrSetWithTime(
key string,
f func() (T, time.Time, error),
) (T, error) {
if val, err := c.Get(key); err == nil || !IsCacheMiss(err) {
return val, err
}

val, t, err := f()
if err != nil {
return val, err
}

return val, c.SetWithTime(key, val, t)
}

// IsCacheMiss returns true if the error is NotFound or Expired.
func IsCacheMiss(err error) bool {
return errors.Is(err, NotFound) || errors.Is(err, Expired)
}

func (c *Cache[T]) filename(key string) string {
dir := filepath.Join(c.cacheDir, c.domain)
_ = os.MkdirAll(dir, 0755)
return filepath.Join(dir, key)
}

0 comments on commit 7b42192

Please sign in to comment.