feat: implemented poke api client with cache
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
package pokeapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/StevanFreeborn/pokedexcli/internal/pokecache"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
http *http.Client
|
||||
baseUrl string
|
||||
cache *pokecache.Cache
|
||||
|
||||
Locations *locationsEndpoint
|
||||
}
|
||||
|
||||
func NewClient(cache *pokecache.Cache) *Client {
|
||||
c := &Client{
|
||||
http: &http.Client{},
|
||||
baseUrl: "https://pokeapi.co/api/v2",
|
||||
cache: cache,
|
||||
}
|
||||
|
||||
c.Locations = &locationsEndpoint{client: c, path: "/location-area"}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Client) NewRequest(method string, path string, queryParams map[string]string, body any) (*http.Request, error) {
|
||||
fullUrl := fmt.Sprintf("%s/%s", strings.TrimRight(c.baseUrl, "/"), strings.TrimLeft(path, "/"))
|
||||
validUrl, err := url.Parse(fullUrl)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse request url: %w", err)
|
||||
}
|
||||
|
||||
query := validUrl.Query()
|
||||
|
||||
for key, value := range queryParams {
|
||||
query.Add(key, value)
|
||||
}
|
||||
|
||||
validUrl.RawQuery = query.Encode()
|
||||
|
||||
var bodyReader io.Reader
|
||||
|
||||
if body != nil {
|
||||
jsonData, err := json.Marshal(body)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request body: %w", err)
|
||||
}
|
||||
|
||||
bodyReader = bytes.NewReader(jsonData)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, validUrl.String(), bodyReader)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create new request: %w", err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
type PokeAPIError struct {
|
||||
StatusCode int
|
||||
BodyText string
|
||||
}
|
||||
|
||||
func (e PokeAPIError) Error() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *Client) handleAPIError(resp *http.Response) error {
|
||||
defer resp.Body.Close()
|
||||
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
|
||||
var bodyText string
|
||||
|
||||
if err != nil {
|
||||
bodyText = string(bodyBytes)
|
||||
}
|
||||
|
||||
return &PokeAPIError{
|
||||
StatusCode: resp.StatusCode,
|
||||
BodyText: bodyText,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) doWithJsonResponse(req *http.Request, v any) error {
|
||||
requestUrl := req.URL.String()
|
||||
cachedData, found := c.cache.Get(requestUrl)
|
||||
|
||||
if found {
|
||||
return json.Unmarshal(cachedData, v)
|
||||
}
|
||||
|
||||
resp, err := c.http.Do(req)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return c.handleAPIError(resp)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
|
||||
c.cache.Add(requestUrl, body)
|
||||
|
||||
return json.Unmarshal(body, v)
|
||||
}
|
||||
|
||||
type Page[T any] struct {
|
||||
Count int `json:"count"`
|
||||
Next string `json:"next"`
|
||||
Previous string `json:"previous"`
|
||||
Results []T `json:"results"`
|
||||
}
|
||||
|
||||
func getOffset(str string) int {
|
||||
valid, err := url.Parse(str)
|
||||
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
stringOffset := valid.Query().Get("offset")
|
||||
offset, err := strconv.Atoi(stringOffset)
|
||||
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return offset
|
||||
}
|
||||
|
||||
func (p *Page[T]) GetNextOffset() int {
|
||||
return getOffset(p.Next)
|
||||
}
|
||||
|
||||
func (p *Page[T]) GetPreviousOffset() int {
|
||||
return getOffset(p.Previous)
|
||||
}
|
||||
|
||||
type Location struct {
|
||||
Name string `json:"name"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
type locationsEndpoint struct {
|
||||
path string
|
||||
client *Client
|
||||
}
|
||||
|
||||
func (le *locationsEndpoint) Get(offset int) (Page[Location], error) {
|
||||
const limit int = 20
|
||||
|
||||
var page Page[Location]
|
||||
|
||||
queryParams := map[string]string{
|
||||
"offset": strconv.Itoa(offset),
|
||||
"limit": strconv.Itoa(limit),
|
||||
}
|
||||
|
||||
req, err := le.client.NewRequest(http.MethodGet, le.path, queryParams, nil)
|
||||
|
||||
if err != nil {
|
||||
return page, err
|
||||
}
|
||||
|
||||
err = le.client.doWithJsonResponse(req, &page)
|
||||
|
||||
if err != nil {
|
||||
return page, err
|
||||
}
|
||||
|
||||
return page, nil
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package pokeapi_test
|
||||
Reference in New Issue
Block a user