Implemented map functions + caching@
This commit is contained in:
parent
54fe7bbffd
commit
f0b303c0c0
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
48
command_mapb.go
Normal 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
|
||||||
|
}
|
||||||
65
internal/pokecache/pokecache.go
Normal file
65
internal/pokecache/pokecache.go
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
71
internal/pokecache/pokecache_test.go
Normal file
71
internal/pokecache/pokecache_test.go
Normal 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
21
repl.go
@ -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,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user