This commit is contained in:
14
main.go
14
main.go
@@ -26,7 +26,7 @@ var commands map[string]cliCommand
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
pokeClient := pokeapi.NewClient(5 * time.Second)
|
pokeClient := pokeapi.NewClient(5*time.Second, time.Minute*5)
|
||||||
cfg := &config{
|
cfg := &config{
|
||||||
pokeapiClient: pokeClient,
|
pokeapiClient: pokeClient,
|
||||||
}
|
}
|
||||||
@@ -84,15 +84,9 @@ func commandExit(*config) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func cleanInput(text string) []string {
|
func cleanInput(text string) []string {
|
||||||
// trim input string
|
output := strings.ToLower(text)
|
||||||
text = strings.TrimSpace(text)
|
words := strings.Fields(output)
|
||||||
// convert to lowercase
|
return words
|
||||||
text = strings.ToLower(text)
|
|
||||||
// replace all double spaces into single space
|
|
||||||
text = strings.Replace(text, " ", " ", -1)
|
|
||||||
// split by space
|
|
||||||
separated := strings.Split(text, " ")
|
|
||||||
return separated
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func printHelpMessage(*config) error {
|
func printHelpMessage(*config) error {
|
||||||
|
|||||||
12
main_test.go
12
main_test.go
@@ -7,10 +7,22 @@ func TestCleanInput(t *testing.T) {
|
|||||||
input string
|
input string
|
||||||
expected []string
|
expected []string
|
||||||
}{
|
}{
|
||||||
|
{
|
||||||
|
input: " ",
|
||||||
|
expected: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: " hello ",
|
||||||
|
expected: []string{"hello"},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
input: " hello world ",
|
input: " hello world ",
|
||||||
expected: []string{"hello", "world"},
|
expected: []string{"hello", "world"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
input: " HellO World ",
|
||||||
|
expected: []string{"hello", "world"},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
input: "Charmander Bulbasaur PIKACHU",
|
input: "Charmander Bulbasaur PIKACHU",
|
||||||
expected: []string{"charmander", "bulbasaur", "pikachu"},
|
expected: []string{"charmander", "bulbasaur", "pikachu"},
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
package pokeapi
|
package pokeapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/rdarius/boot-dev-pokedex/pokecache"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client -
|
// Client -
|
||||||
type Client struct {
|
type Client struct {
|
||||||
|
cache pokecache.Cache
|
||||||
httpClient http.Client
|
httpClient http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient -
|
// NewClient -
|
||||||
func NewClient(timeout time.Duration) Client {
|
func NewClient(timeout, cacheInterval time.Duration) Client {
|
||||||
return Client{
|
return Client{
|
||||||
|
cache: pokecache.NewCache(cacheInterval),
|
||||||
httpClient: http.Client{
|
httpClient: http.Client{
|
||||||
Timeout: timeout,
|
Timeout: timeout,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,6 +13,16 @@ func (c *Client) ListLocations(pageURL *string) (RespShallowLocations, error) {
|
|||||||
url = *pageURL
|
url = *pageURL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if val, ok := c.cache.Get(url); ok {
|
||||||
|
locationsResp := RespShallowLocations{}
|
||||||
|
err := json.Unmarshal(val, &locationsResp)
|
||||||
|
if err != nil {
|
||||||
|
return RespShallowLocations{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return locationsResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return RespShallowLocations{}, err
|
return RespShallowLocations{}, err
|
||||||
@@ -22,7 +32,6 @@ func (c *Client) ListLocations(pageURL *string) (RespShallowLocations, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return RespShallowLocations{}, err
|
return RespShallowLocations{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func(Body io.ReadCloser) {
|
defer func(Body io.ReadCloser) {
|
||||||
_ = Body.Close()
|
_ = Body.Close()
|
||||||
}(resp.Body)
|
}(resp.Body)
|
||||||
@@ -38,5 +47,6 @@ func (c *Client) ListLocations(pageURL *string) (RespShallowLocations, error) {
|
|||||||
return RespShallowLocations{}, err
|
return RespShallowLocations{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.cache.Add(url, dat)
|
||||||
return locationsResp, nil
|
return locationsResp, nil
|
||||||
}
|
}
|
||||||
|
|||||||
64
pokecache/pokecache.go
Normal file
64
pokecache/pokecache.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package pokecache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cache -
|
||||||
|
type Cache struct {
|
||||||
|
cache map[string]cacheEntry
|
||||||
|
mux *sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type cacheEntry struct {
|
||||||
|
createdAt time.Time
|
||||||
|
val []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCache -
|
||||||
|
func NewCache(interval time.Duration) Cache {
|
||||||
|
c := Cache{
|
||||||
|
cache: make(map[string]cacheEntry),
|
||||||
|
mux: &sync.Mutex{},
|
||||||
|
}
|
||||||
|
|
||||||
|
go c.reapLoop(interval)
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add -
|
||||||
|
func (c *Cache) Add(key string, value []byte) {
|
||||||
|
c.mux.Lock()
|
||||||
|
defer c.mux.Unlock()
|
||||||
|
c.cache[key] = cacheEntry{
|
||||||
|
createdAt: time.Now().UTC(),
|
||||||
|
val: value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get -
|
||||||
|
func (c *Cache) Get(key string) ([]byte, bool) {
|
||||||
|
c.mux.Lock()
|
||||||
|
defer c.mux.Unlock()
|
||||||
|
val, ok := c.cache[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.cache {
|
||||||
|
if v.createdAt.Before(now.Add(-last)) {
|
||||||
|
delete(c.cache, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
61
pokecache/pokecache_test.go
Normal file
61
pokecache/pokecache_test.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
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")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user