From e51545a0c3ec530af84415e2059e10d8d4b68147 Mon Sep 17 00:00:00 2001 From: Enrique Ordaz Date: Thu, 5 Jun 2025 16:09:14 -0600 Subject: [PATCH] Added pokemon encounter struct --- command_map.go | 6 +-- internal/pokeapi/client.go | 6 ++- internal/pokeapi/location_list.go | 32 +++++++++----- internal/pokeapi/types_pokemons.go | 12 ++++++ internal/pokecache/pokecache.go | 63 ++++++++++++++++++++++++++++ internal/pokecache/pokecache_test.go | 60 ++++++++++++++++++++++++++ main.go | 2 +- 7 files changed, 166 insertions(+), 15 deletions(-) create mode 100644 internal/pokeapi/types_pokemons.go create mode 100644 internal/pokecache/pokecache.go create mode 100644 internal/pokecache/pokecache_test.go diff --git a/command_map.go b/command_map.go index e4a4db8..315027e 100644 --- a/command_map.go +++ b/command_map.go @@ -6,9 +6,9 @@ import ( ) func commandMap(cfg *config) error { - if cfg.nextLocationsURL == nil { - return errors.New("you're on the last page") - } + // if cfg.nextLocationsURL == nil { + // return errors.New("you're on the last page") + // } locationsResp, err := cfg.pokeClient.ListLocations(cfg.nextLocationsURL) if err != nil { diff --git a/internal/pokeapi/client.go b/internal/pokeapi/client.go index a2c36d9..87605c9 100644 --- a/internal/pokeapi/client.go +++ b/internal/pokeapi/client.go @@ -3,16 +3,20 @@ package pokeapi import ( "net/http" "time" + + "git.eo13-dev.com/enordaz/gokedex/internal/pokecache" ) type Client struct { httpClient http.Client + cache pokecache.Cache } -func NewClient(timeout time.Duration) Client { +func NewClient(timeout, cacheInterval time.Duration) Client { return Client{ httpClient: http.Client{ Timeout: timeout, }, + cache: pokecache.NewCache(cacheInterval), } } diff --git a/internal/pokeapi/location_list.go b/internal/pokeapi/location_list.go index c6dd218..a601c8c 100644 --- a/internal/pokeapi/location_list.go +++ b/internal/pokeapi/location_list.go @@ -1,7 +1,9 @@ package pokeapi import ( + "bytes" "encoding/json" + "io" "net/http" ) @@ -11,20 +13,30 @@ func (c *Client) ListLocations(pageUrl *string) (RespShallowLocations, error) { url = *pageUrl } - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return RespShallowLocations{}, err - } + data, ok := c.cache.Get(url) + if !ok { - resp, err := c.httpClient.Do(req) - if err != nil { - return RespShallowLocations{}, err + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return RespShallowLocations{}, err + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return RespShallowLocations{}, err + } + defer resp.Body.Close() + + data, err = io.ReadAll(resp.Body) + if err != nil { + return RespShallowLocations{}, err + } + c.cache.Add(url, data) } - defer resp.Body.Close() var locationsResp RespShallowLocations - decoder := json.NewDecoder(resp.Body) - if err = decoder.Decode(&locationsResp); err != nil { + decoder := json.NewDecoder(bytes.NewReader(data)) + if err := decoder.Decode(&locationsResp); err != nil { return RespShallowLocations{}, err } diff --git a/internal/pokeapi/types_pokemons.go b/internal/pokeapi/types_pokemons.go new file mode 100644 index 0000000..a1fefeb --- /dev/null +++ b/internal/pokeapi/types_pokemons.go @@ -0,0 +1,12 @@ +package pokeapi + +type RespShallowLocationExplore struct { + Id int `json:"id"` + Name string `json:"name"` + Encounters []struct { + Pokemon struct { + Name string `json:"name"` + URL string `json:"url"` + } `json:"pokemon"` + } +} diff --git a/internal/pokecache/pokecache.go b/internal/pokecache/pokecache.go new file mode 100644 index 0000000..a3f417f --- /dev/null +++ b/internal/pokecache/pokecache.go @@ -0,0 +1,63 @@ +package pokecache + +import ( + "sync" + "time" +) + +type Cache struct { + entries map[string]cacheEntry + mux *sync.RWMutex +} + +type cacheEntry struct { + createdAt time.Time + val []byte +} + +func NewCache(interval time.Duration) Cache { + cache := Cache{ + entries: make(map[string]cacheEntry), + mux: &sync.RWMutex{}, + } + + go cache.reapLoop(interval) + + return cache +} + +func (c *Cache) Add(key string, value []byte) { + c.mux.Lock() + defer c.mux.Unlock() + + c.entries[key] = cacheEntry{ + createdAt: time.Now().UTC(), + val: value, + } +} + +func (c *Cache) Get(key string) ([]byte, bool) { + c.mux.RLock() + defer c.mux.RUnlock() + + val, ok := c.entries[key] + return val.val, ok +} + +func (c *Cache) reapLoop(interval time.Duration) { + ticker := time.NewTicker(interval) + for range ticker.C { + c.reap(time.Now().UTC(), interval) + } +} + +func (c *Cache) reap(now time.Time, last time.Duration) { + c.mux.Lock() + defer c.mux.Unlock() + + for k, v := range c.entries { + if v.createdAt.Before(now.Add(-last)) { + delete(c.entries, k) + } + } +} diff --git a/internal/pokecache/pokecache_test.go b/internal/pokecache/pokecache_test.go new file mode 100644 index 0000000..d582a72 --- /dev/null +++ b/internal/pokecache/pokecache_test.go @@ -0,0 +1,60 @@ +package pokecache + +import ( + "fmt" + "testing" + "time" +) + +func TestAddGet(t *testing.T) { + const interval = 5 * time.Second + cases := []struct { + key string + val []byte + }{ + { + key: "https://example.com", + val: []byte("testdata"), + }, + { + key: "https://example.com/path", + val: []byte("moretestdata"), + }, + } + + for i, c := range cases { + t.Run(fmt.Sprintf("Test case %v", i), func(t *testing.T) { + cache := NewCache(interval) + cache.Add(c.key, c.val) + val, ok := cache.Get(c.key) + if !ok { + t.Errorf("expected to find key") + return + } + if string(val) != string(c.val) { + t.Errorf("expected to find value") + } + }) + } +} + +func TestReapLoop(t *testing.T) { + const baseTime = 5 * time.Millisecond + const waitTime = baseTime + 5*time.Millisecond + cache := NewCache(baseTime) + cache.Add("https://example.com", []byte("testdata")) + + _, ok := cache.Get("https://example.com") + if !ok { + t.Errorf("expected to find key") + return + } + + time.Sleep(waitTime) + + _, ok = cache.Get("https://example.com") + if ok { + t.Errorf("expected to not fing key") + return + } +} diff --git a/main.go b/main.go index d5f3bce..c19495d 100644 --- a/main.go +++ b/main.go @@ -9,7 +9,7 @@ import ( const prompt = "Pokedex > " func main() { - pokeClient := pokeapi.NewClient(5 * time.Second) + pokeClient := pokeapi.NewClient(5*time.Second, 5*time.Minute) cfg := &config{ pokeClient: pokeClient, }