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 Pokemon *pokemonEndpoint } 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"} c.Pokemon = &pokemonEndpoint{client: c, path: "/pokemon"} 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 EncounteredPokemon struct { Name string `json:"name"` } type PokemonEncounter struct { Pokemon EncounteredPokemon `json:"pokemon"` } type LocationArea struct { PokemonEncounters []PokemonEncounter `json:"pokemon_encounters"` } type locationsEndpoint struct { path string client *Client } func (le *locationsEndpoint) Get(name string) (LocationArea, error) { var area LocationArea requestUrl := fmt.Sprintf("%s/%s", le.path, name) req, err := le.client.NewRequest(http.MethodGet, requestUrl, map[string]string{}, nil) if err != nil { return area, err } err = le.client.doWithJsonResponse(req, &area) if err != nil { return area, err } return area, nil } func (le *locationsEndpoint) List(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 } type Stat struct { Name string `json:"name"` } type PokemonStat struct { BaseStat int `json:"base_stat"` Effort int `json:"effort"` Stat Stat `json:"stat"` } type Type struct { Name string `json:"name"` } type PokemonType struct { Type Type `json:"type"` } type Pokemon struct { Id int `json:"id"` Name string `json:"name"` Height int `json:"height"` Weight int `json:"weight"` Stats []PokemonStat `json:"stats"` Types []PokemonType `json:"types"` BaseExperience int `json:"base_experience"` } type pokemonEndpoint struct { path string client *Client } func (pe *pokemonEndpoint) Get(name string) (Pokemon, error) { var pokemon Pokemon requestUrl := fmt.Sprintf("%s/%s", pe.path, name) req, err := pe.client.NewRequest(http.MethodGet, requestUrl, map[string]string{}, nil) if err != nil { return pokemon, err } err = pe.client.doWithJsonResponse(req, &pokemon) if err != nil { return pokemon, err } return pokemon, nil }