checkpoint: add explore command

Add the explore command to list the pokemon in a given location area.
This commit is contained in:
Dan Anglin 2024-09-20 00:09:12 +01:00
parent b6088bfd88
commit 578c781907
Signed by: dananglin
GPG key ID: 0C1D44CFBEE68638
3 changed files with 180 additions and 65 deletions

View file

@ -1,50 +1,50 @@
package pokeapi package pokeapi
// LocationArea is a section of areas such as floors in a building or a cave. // LocationArea is a section of areas such as floors in a building or a cave.
//type LocationArea struct { type LocationArea struct {
// ID int `json:"id"` ID int `json:"id"`
// Name string `json:"name"` Name string `json:"name"`
// GameIndex int `json:"game_index"` GameIndex int `json:"game_index"`
// EncounterMethodRates []EncounterMethodRate `json:"encounter_method_rates"` EncounterMethodRates []EncounterMethodRate `json:"encounter_method_rates"`
// Location NamedAPIResource `json:"location"` Location NamedAPIResource `json:"location"`
// Names []Name `json:"names"` Names []Name `json:"names"`
// PokemonEncounters []PokemonEncounter `json:"pokemon_encounters"` PokemonEncounters []PokemonEncounter `json:"pokemon_encounters"`
//} }
//
//type EncounterMethodRate struct { type EncounterMethodRate struct {
// EncounterMethod NamedAPIResource `json:"encounter_method"` EncounterMethod NamedAPIResource `json:"encounter_method"`
// VersionDetails EncounterVersionDetails `json:"version_details"` VersionDetails []EncounterVersionDetails `json:"version_details"`
//} }
//
//type EncounterVersionDetails struct { type EncounterVersionDetails struct {
// Rate int `json:"rate"` Rate int `json:"rate"`
// Version NamedAPIResource `json:"version"` Version NamedAPIResource `json:"version"`
//} }
//
//type Name struct { type Name struct {
// Name string `json:"name"` Name string `json:"name"`
// Language NamedAPIResource `json:"language"` Language NamedAPIResource `json:"language"`
//} }
//
//// PokemonEncounter is details of a possible Pokemon encounter. // PokemonEncounter is details of a possible Pokemon encounter.
//type PokemonEncounter struct { type PokemonEncounter struct {
// Pokemon NamedAPIResource `json:"pokemon"` Pokemon NamedAPIResource `json:"pokemon"`
// VersionDetails []VersionEncounterDetails `json:"version_details"` VersionDetails []VersionEncounterDetails `json:"version_details"`
//} }
//
//type VersionEncounterDetails struct { type VersionEncounterDetails struct {
// Version NamedAPIResource `json:"version"` Version NamedAPIResource `json:"version"`
// MaxChance int `json:"max_chance"` MaxChance int `json:"max_chance"`
// EncounterDetails []Encounter `json:"encounter_details"` EncounterDetails []Encounter `json:"encounter_details"`
//} }
//
//type Encounter struct { type Encounter struct {
// MinLevel int `json:"min_level"` MinLevel int `json:"min_level"`
// MaxLevel int `json:"max_level"` MaxLevel int `json:"max_level"`
// ConditionValues []NamedAPIResource `json:"condition_values"` ConditionValues []NamedAPIResource `json:"condition_values"`
// Chance int `json:"chance"` Chance int `json:"chance"`
// Method NamedAPIResource `json:"method"` Method NamedAPIResource `json:"method"`
//} }
type NamedAPIResourceList struct { type NamedAPIResourceList struct {
Count int `json:"count"` Count int `json:"count"`

View file

@ -12,6 +12,12 @@ import (
"codeflow.dananglin.me.uk/apollo/pokedex/internal/pokecache" "codeflow.dananglin.me.uk/apollo/pokedex/internal/pokecache"
) )
const (
baseURL string = "https://pokeapi.co/api/v2"
LocationAreaEndpoint = baseURL + "/location-area"
)
type Client struct { type Client struct {
httpClient http.Client httpClient http.Client
cache *pokecache.Cache cache *pokecache.Cache
@ -38,7 +44,7 @@ func (c *Client) GetNamedAPIResourceList(url string) (pokeapi.NamedAPIResourceLi
fmt.Println("Using data from cache.") fmt.Println("Using data from cache.")
if err := decodeJSON(dataFromCache, &list); err != nil { if err := decodeJSON(dataFromCache, &list); err != nil {
return pokeapi.NamedAPIResourceList{}, fmt.Errorf("unable to decode the data from cache: %w", err) return pokeapi.NamedAPIResourceList{}, fmt.Errorf("unable to decode the data from the cache: %w", err)
} }
return list, nil return list, nil
@ -84,6 +90,62 @@ func (c *Client) GetNamedAPIResourceList(url string) (pokeapi.NamedAPIResourceLi
return list, nil return list, nil
} }
func (c *Client) GetLocationArea(location string) (pokeapi.LocationArea, error) {
var locationArea pokeapi.LocationArea
url := LocationAreaEndpoint + "/" + location + "/"
dataFromCache, exists := c.cache.Get(url)
if exists {
fmt.Println("Using data from cache.")
if err := decodeJSON(dataFromCache, &locationArea); err != nil {
return pokeapi.LocationArea{}, fmt.Errorf("unable to decode the data from the cache: %w", err)
}
return locationArea, nil
}
ctx, cancel := context.WithTimeout(context.Background(), c.timeout)
defer cancel()
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return pokeapi.LocationArea{}, fmt.Errorf("error creating the HTTP request: %w", err)
}
resp, err := c.httpClient.Do(request)
if err != nil {
return pokeapi.LocationArea{}, fmt.Errorf("error getting the response from the server: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return pokeapi.LocationArea{}, fmt.Errorf(
"received a bad status from %s: (%d) %s",
url,
resp.StatusCode,
resp.Status,
)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return pokeapi.LocationArea{}, fmt.Errorf(
"unable to read the response from the server: %w",
err,
)
}
if err := decodeJSON(body, &locationArea); err != nil {
return pokeapi.LocationArea{}, fmt.Errorf("unable to decode the data from the server: %w", err)
}
c.cache.Add(url, body)
return locationArea, nil
}
func decodeJSON(data []byte, value any) error { func decodeJSON(data []byte, value any) error {
if err := json.Unmarshal(data, value); err != nil { if err := json.Unmarshal(data, value); err != nil {
return fmt.Errorf("unable to decode the JSON data: %w", err) return fmt.Errorf("unable to decode the JSON data: %w", err)

93
main.go
View file

@ -2,20 +2,17 @@ package main
import ( import (
"bufio" "bufio"
"errors"
"fmt" "fmt"
"maps" "maps"
"os" "os"
"slices" "slices"
"strings"
"time" "time"
"codeflow.dananglin.me.uk/apollo/pokedex/internal/pokeclient" "codeflow.dananglin.me.uk/apollo/pokedex/internal/pokeclient"
) )
const (
baseURL string = "https://pokeapi.co/api/v2"
locationAreaEndpoint string = "/location-area"
)
type State struct { type State struct {
Previous *string Previous *string
Next *string Next *string
@ -29,7 +26,7 @@ type command struct {
callback callbackFunc callback callbackFunc
} }
type callbackFunc func() error type callbackFunc func(args []string) error
func main() { func main() {
run() run()
@ -45,7 +42,7 @@ func run() {
"exit": { "exit": {
name: "exit", name: "exit",
description: "Exit the Pokedex", description: "Exit the Pokedex",
callback: commandExit, callback: exitFunc,
}, },
"help": { "help": {
name: "help", name: "help",
@ -55,12 +52,17 @@ func run() {
"map": { "map": {
name: "map", name: "map",
description: "Displays the next 20 locations in the Pokemon world", description: "Displays the next 20 locations in the Pokemon world",
callback: commandMap(client), callback: mapFunc(client),
}, },
"mapb": { "mapb": {
name: "map back", name: "map back",
description: "Displays the previous 20 locations in the Pokemon world", description: "Displays the previous 20 locations in the Pokemon world",
callback: commandMapB(client), callback: mapBFunc(client),
},
"explore": {
name: "explore",
description: "Lists all the pokemon in a given area",
callback: exploreFunc(client),
}, },
} }
@ -69,7 +71,7 @@ func run() {
commandMap["help"] = command{ commandMap["help"] = command{
name: "help", name: "help",
description: "Displays a help message", description: "Displays a help message",
callback: commandHelp(summaries), callback: helpFunc(summaries),
} }
fmt.Printf("\nWelcome to the Pokedex!\n") fmt.Printf("\nWelcome to the Pokedex!\n")
@ -78,7 +80,9 @@ func run() {
scanner := bufio.NewScanner(os.Stdin) scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() { for scanner.Scan() {
command := scanner.Text() input := scanner.Text()
command, args := parseArgs(input)
cmd, ok := commandMap[command] cmd, ok := commandMap[command]
if !ok { if !ok {
@ -97,7 +101,7 @@ func run() {
continue continue
} }
if err := commandMap[command].callback(); err != nil { if err := commandMap[command].callback(args); err != nil {
fmt.Printf("ERROR: %v.\n", err) fmt.Printf("ERROR: %v.\n", err)
} }
@ -105,8 +109,8 @@ func run() {
} }
} }
func commandHelp(summaries map[string]string) callbackFunc { func helpFunc(summaries map[string]string) callbackFunc {
return func() error { return func(_ []string) error {
keys := []string{} keys := []string{}
for key := range maps.All(summaries) { for key := range maps.All(summaries) {
@ -127,26 +131,26 @@ func commandHelp(summaries map[string]string) callbackFunc {
} }
} }
func commandExit() error { func exitFunc(_ []string) error {
os.Exit(0) os.Exit(0)
return nil return nil
} }
func commandMap(client *pokeclient.Client) callbackFunc { func mapFunc(client *pokeclient.Client) callbackFunc {
return func() error { return func(_ []string) error {
url := state.Next url := state.Next
if url == nil { if url == nil {
url = new(string) url = new(string)
*url = baseURL + locationAreaEndpoint *url = pokeclient.LocationAreaEndpoint
} }
return printResourceList(client, *url) return printResourceList(client, *url)
} }
} }
func commandMapB(client *pokeclient.Client) callbackFunc { func mapBFunc(client *pokeclient.Client) callbackFunc {
return func() error { return func(_ []string) error {
url := state.Previous url := state.Previous
if url == nil { if url == nil {
return fmt.Errorf("no previous locations available") return fmt.Errorf("no previous locations available")
@ -156,6 +160,41 @@ func commandMapB(client *pokeclient.Client) callbackFunc {
} }
} }
func exploreFunc(client *pokeclient.Client) callbackFunc {
return func(args []string) error {
if args == nil {
return errors.New("the location has not been specified")
}
if len(args) != 1 {
return fmt.Errorf(
"unexpected number of locations: want 1; got %d",
len(args),
)
}
location := args[0]
fmt.Println("Exploring", location)
locationArea, err := client.GetLocationArea(location)
if err != nil {
return fmt.Errorf(
"unable to get the location area: %w",
err,
)
}
fmt.Println("Found Pokemon:")
for _, encounter := range slices.All(locationArea.PokemonEncounters) {
fmt.Printf("- %s\n", encounter.Pokemon.Name)
}
return nil
}
}
func printResourceList(client *pokeclient.Client, url string) error { func printResourceList(client *pokeclient.Client, url string) error {
list, err := client.GetNamedAPIResourceList(url) list, err := client.GetNamedAPIResourceList(url)
if err != nil { if err != nil {
@ -181,3 +220,17 @@ func summaryMap(commandMap map[string]command) map[string]string {
return summaries return summaries
} }
func parseArgs(input string) (string, []string) {
split := strings.Split(input, " ")
if len(split) == 0 {
return "", nil
}
if len(split) == 1 {
return split[0], nil
}
return split[0], split[1:]
}