Implemented map functions + caching@

This commit is contained in:
V 2025-10-04 18:29:20 +01:00
parent 54fe7bbffd
commit f0b303c0c0
7 changed files with 252 additions and 6 deletions

View File

@ -3,9 +3,10 @@ package main
import ( import (
"fmt" "fmt"
"os" "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!") fmt.Println("Closing the Pokedex... Goodbye!")
os.Exit(0) os.Exit(0)
return nil return nil

View File

@ -2,10 +2,11 @@ package main
import ( import (
"fmt" "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") fmt.Print("Pokedex CLI - available commands:\n\n")
for _, v := range getCommands() { for _, v := range getCommands() {
fmt.Printf("%s %s\n", v.name, v.description) fmt.Printf("%s %s\n", v.name, v.description)

View File

@ -1,5 +1,50 @@
package main 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 return nil
} }

48
command_mapb.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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
}
}

21
repl.go
View File

@ -5,12 +5,20 @@ import (
"strings" "strings"
"bufio" "bufio"
"os" "os"
"k3gtpi.jumpingcrab.com/go-learning/pokedexcli/internal/pokecache"
"time"
) )
type cliCommand struct { type cliCommand struct {
name string name string
description 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 var supportedCommands map[string]cliCommand
@ -18,6 +26,8 @@ var supportedCommands map[string]cliCommand
func startRepl() { func startRepl() {
reader := bufio.NewScanner(os.Stdin) reader := bufio.NewScanner(os.Stdin)
fmt.Println("Welcome to the Pokedex!") fmt.Println("Welcome to the Pokedex!")
pokedexConfig := PokedexConfig{}
cache := pokecache.NewCache(5 * time.Second)
for { for {
// Print prompt // Print prompt
fmt.Printf("Pokedex > ") fmt.Printf("Pokedex > ")
@ -35,8 +45,8 @@ func startRepl() {
fmt.Println("Unknown command.") fmt.Println("Unknown command.")
continue continue
} }
if err := command.callback(); err != nil { if err := command.callback(&pokedexConfig, cache); err != nil {
fmt.Printf("Encountered error running command: %v\n", command.name) 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.", description: "Print Pokemon world locations.",
callback: commandMap, callback: commandMap,
}, },
"mapb": {
name: "mapb",
description: "Print previoud Pokemon locationsi.",
callback: commandMapb,
},
} }
} }