Implemented map functions + caching@
This commit is contained in:
		
							parent
							
								
									54fe7bbffd
								
							
						
					
					
						commit
						f0b303c0c0
					
				@ -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
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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"
 | 
			
		||||
	"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,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user