From f0b303c0c081fe40a2ee54fdc3302918a25452a6 Mon Sep 17 00:00:00 2001 From: V Date: Sat, 4 Oct 2025 18:29:20 +0100 Subject: [PATCH] Implemented map functions + caching@ --- command_exit.go | 3 +- command_help.go | 3 +- command_map.go | 47 +++++++++++++++++- command_mapb.go | 48 +++++++++++++++++++ internal/pokecache/pokecache.go | 65 +++++++++++++++++++++++++ internal/pokecache/pokecache_test.go | 71 ++++++++++++++++++++++++++++ repl.go | 21 ++++++-- 7 files changed, 252 insertions(+), 6 deletions(-) create mode 100644 command_mapb.go create mode 100644 internal/pokecache/pokecache.go create mode 100644 internal/pokecache/pokecache_test.go diff --git a/command_exit.go b/command_exit.go index ca866ff..fbf1360 100644 --- a/command_exit.go +++ b/command_exit.go @@ -3,9 +3,10 @@ package main import ( "fmt" "os" + "k3gtpi.jumpingcrab.com/go-learning/pokedexcli/internal/pokecache" ) -func commandExit() error { +func commandExit(p *PokedexConfig, c *pokecache.Cache) error { fmt.Println("Closing the Pokedex... Goodbye!") os.Exit(0) return nil diff --git a/command_help.go b/command_help.go index 6e389cf..b099c01 100644 --- a/command_help.go +++ b/command_help.go @@ -2,10 +2,11 @@ package main import ( "fmt" + "k3gtpi.jumpingcrab.com/go-learning/pokedexcli/internal/pokecache" ) -func commandHelp() error { +func commandHelp(p *PokedexConfig, c *pokecache.Cache) error { fmt.Print("Pokedex CLI - available commands:\n\n") for _, v := range getCommands() { fmt.Printf("%s %s\n", v.name, v.description) diff --git a/command_map.go b/command_map.go index ad5acfd..1e3d73e 100644 --- a/command_map.go +++ b/command_map.go @@ -1,5 +1,50 @@ package main -func commandMap() error { +import ( + "fmt" + "net/http" + "encoding/json" + "io" + "k3gtpi.jumpingcrab.com/go-learning/pokedexcli/internal/pokecache" +) + + +func commandMap(p *PokedexConfig, c *pokecache.Cache) error { + var baseUrl string + if p.Next == nil { + baseUrl = "https://pokeapi.co/api/v2/location-area/" + } else { + baseUrl = *p.Next + } + + var body []byte + // Check if respones is available in cache + if resp, exists := c.Get(baseUrl); exists { + body = resp + } else { + client := &http.Client{} + + resp, err := client.Get(baseUrl) + if err != nil { + return fmt.Errorf("could not make request to Pokedex API! Err: %w", err) + } + + body, err = io.ReadAll(resp.Body) + defer resp.Body.Close() + if resp.StatusCode > 299 { + return fmt.Errorf("request returned non-200 code! Code: %v Body: %v", resp.StatusCode, body) + } + if err != nil { + return fmt.Errorf("could not read request body! Err: %w", err) + } + c.Add(baseUrl, body) + } + + if err := json.Unmarshal(body, &p); err != nil { + return fmt.Errorf("could not unmarshal response! Err: %w", err) + } + for _, location := range p.Results { + fmt.Println(location["name"]) + } return nil } diff --git a/command_mapb.go b/command_mapb.go new file mode 100644 index 0000000..1d29fe2 --- /dev/null +++ b/command_mapb.go @@ -0,0 +1,48 @@ +package main + +import ( + "fmt" + "io" + "encoding/json" + "net/http" + "k3gtpi.jumpingcrab.com/go-learning/pokedexcli/internal/pokecache" +) + +func commandMapb(p *PokedexConfig, c *pokecache.Cache) error { + var baseUrl string + if p.Previous == nil { + fmt.Println("you're on the first page") + return nil + } else { + baseUrl = *p.Previous + } + var body []byte + if resp, exists := c.Get(baseUrl); exists { + body = resp + } else { + client := &http.Client{} + + resp, err := client.Get(baseUrl) + if err != nil { + return fmt.Errorf("could not make request to Pokedex API! Err: %w", err) + } + + body, err = io.ReadAll(resp.Body) + defer resp.Body.Close() + if resp.StatusCode > 299 { + return fmt.Errorf("request returned non-200 code! Code: %v Body: %v", resp.StatusCode, body) + } + if err != nil { + return fmt.Errorf("could not read request body! Err: %w", err) + } + + c.Add(baseUrl, body) + } + if err := json.Unmarshal(body, &p); err != nil { + return fmt.Errorf("could not unmarshal response! Err: %w", err) + } + for _, location := range p.Results { + fmt.Println(location["name"]) + } + return nil +} diff --git a/internal/pokecache/pokecache.go b/internal/pokecache/pokecache.go new file mode 100644 index 0000000..ce2d320 --- /dev/null +++ b/internal/pokecache/pokecache.go @@ -0,0 +1,65 @@ +package pokecache + +import ( + "time" + "sync" +) + +type cacheEntry struct { + createdAt time.Time + val []byte +} + +type Cache struct { + PokeCache map[string]cacheEntry + Interval time.Duration + Mu sync.Mutex +} + +func (c *Cache) Add(key string, val []byte) { + newEntry := cacheEntry{ + createdAt: time.Now(), + val: val, + } + c.Mu.Lock() + defer c.Mu.Unlock() + c.PokeCache[key] = newEntry +} + +func (c *Cache) Get(key string) ([]byte, bool) { + c.Mu.Lock() + + defer c.Mu.Unlock() + cache, exists := c.PokeCache[key] + if !exists { + return nil, false + } + return cache.val, true +} + +func (c *Cache) reapLoop() { + ticker := time.NewTicker(c.Interval) + defer ticker.Stop() + + for range ticker.C { + c.Mu.Lock() + for k, v := range c.PokeCache { + if time.Since(v.createdAt) > c.Interval { + delete(c.PokeCache, k) + } + } + c.Mu.Unlock() + } +} + +func NewCache(interval time.Duration) *Cache { + newCache := Cache{ + PokeCache: map[string]cacheEntry{}, + Interval: interval, + Mu: sync.Mutex{}, + } + go newCache.reapLoop() + return &newCache +} + + diff --git a/internal/pokecache/pokecache_test.go b/internal/pokecache/pokecache_test.go new file mode 100644 index 0000000..9758829 --- /dev/null +++ b/internal/pokecache/pokecache_test.go @@ -0,0 +1,71 @@ +package pokecache + +import ( + "testing" + "time" + "fmt" +) + + +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") + return + } + }) + } +} + +func TestGetNonexistent(t *testing.T) { + const interval = 2 * time.Second + cache := NewCache(interval) + value, ok := cache.Get("http://does.not.exist/") + if ok || (value != nil) { + t.Errorf("expected cache miss") + } +} + +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 find key") + return + } +} diff --git a/repl.go b/repl.go index 34d282c..86db711 100644 --- a/repl.go +++ b/repl.go @@ -5,12 +5,20 @@ import ( "strings" "bufio" "os" + "k3gtpi.jumpingcrab.com/go-learning/pokedexcli/internal/pokecache" + "time" ) type cliCommand struct { name string description string - callback func() error + callback func(*PokedexConfig, *pokecache.Cache) error +} + +type PokedexConfig struct { + Next *string + Previous *string + Results []map[string]string } var supportedCommands map[string]cliCommand @@ -18,6 +26,8 @@ var supportedCommands map[string]cliCommand func startRepl() { reader := bufio.NewScanner(os.Stdin) fmt.Println("Welcome to the Pokedex!") + pokedexConfig := PokedexConfig{} + cache := pokecache.NewCache(5 * time.Second) for { // Print prompt fmt.Printf("Pokedex > ") @@ -35,8 +45,8 @@ func startRepl() { fmt.Println("Unknown command.") continue } - if err := command.callback(); err != nil { - fmt.Printf("Encountered error running command: %v\n", command.name) + if err := command.callback(&pokedexConfig, cache); err != nil { + fmt.Printf("Encountered error running command: %v\nErr: %v", command.name, err) } } } @@ -68,5 +78,10 @@ func getCommands() map[string]cliCommand{ description: "Print Pokemon world locations.", callback: commandMap, }, + "mapb": { + name: "mapb", + description: "Print previoud Pokemon locationsi.", + callback: commandMapb, + }, } }