From 578c7819073fca7fc15246b8024bf0c4abe93124 Mon Sep 17 00:00:00 2001 From: Dan Anglin Date: Fri, 20 Sep 2024 00:09:12 +0100 Subject: [PATCH] checkpoint: add explore command Add the explore command to list the pokemon in a given location area. --- internal/api/pokeapi/types.go | 88 ++++++++++++++--------------- internal/pokeclient/pokeclient.go | 64 ++++++++++++++++++++- main.go | 93 ++++++++++++++++++++++++------- 3 files changed, 180 insertions(+), 65 deletions(-) diff --git a/internal/api/pokeapi/types.go b/internal/api/pokeapi/types.go index 85a23c0..bd3985c 100644 --- a/internal/api/pokeapi/types.go +++ b/internal/api/pokeapi/types.go @@ -1,50 +1,50 @@ package pokeapi // LocationArea is a section of areas such as floors in a building or a cave. -//type LocationArea struct { -// ID int `json:"id"` -// Name string `json:"name"` -// GameIndex int `json:"game_index"` -// EncounterMethodRates []EncounterMethodRate `json:"encounter_method_rates"` -// Location NamedAPIResource `json:"location"` -// Names []Name `json:"names"` -// PokemonEncounters []PokemonEncounter `json:"pokemon_encounters"` -//} -// -//type EncounterMethodRate struct { -// EncounterMethod NamedAPIResource `json:"encounter_method"` -// VersionDetails EncounterVersionDetails `json:"version_details"` -//} -// -//type EncounterVersionDetails struct { -// Rate int `json:"rate"` -// Version NamedAPIResource `json:"version"` -//} -// -//type Name struct { -// Name string `json:"name"` -// Language NamedAPIResource `json:"language"` -//} -// -//// PokemonEncounter is details of a possible Pokemon encounter. -//type PokemonEncounter struct { -// Pokemon NamedAPIResource `json:"pokemon"` -// VersionDetails []VersionEncounterDetails `json:"version_details"` -//} -// -//type VersionEncounterDetails struct { -// Version NamedAPIResource `json:"version"` -// MaxChance int `json:"max_chance"` -// EncounterDetails []Encounter `json:"encounter_details"` -//} -// -//type Encounter struct { -// MinLevel int `json:"min_level"` -// MaxLevel int `json:"max_level"` -// ConditionValues []NamedAPIResource `json:"condition_values"` -// Chance int `json:"chance"` -// Method NamedAPIResource `json:"method"` -//} +type LocationArea struct { + ID int `json:"id"` + Name string `json:"name"` + GameIndex int `json:"game_index"` + EncounterMethodRates []EncounterMethodRate `json:"encounter_method_rates"` + Location NamedAPIResource `json:"location"` + Names []Name `json:"names"` + PokemonEncounters []PokemonEncounter `json:"pokemon_encounters"` +} + +type EncounterMethodRate struct { + EncounterMethod NamedAPIResource `json:"encounter_method"` + VersionDetails []EncounterVersionDetails `json:"version_details"` +} + +type EncounterVersionDetails struct { + Rate int `json:"rate"` + Version NamedAPIResource `json:"version"` +} + +type Name struct { + Name string `json:"name"` + Language NamedAPIResource `json:"language"` +} + +// PokemonEncounter is details of a possible Pokemon encounter. +type PokemonEncounter struct { + Pokemon NamedAPIResource `json:"pokemon"` + VersionDetails []VersionEncounterDetails `json:"version_details"` +} + +type VersionEncounterDetails struct { + Version NamedAPIResource `json:"version"` + MaxChance int `json:"max_chance"` + EncounterDetails []Encounter `json:"encounter_details"` +} + +type Encounter struct { + MinLevel int `json:"min_level"` + MaxLevel int `json:"max_level"` + ConditionValues []NamedAPIResource `json:"condition_values"` + Chance int `json:"chance"` + Method NamedAPIResource `json:"method"` +} type NamedAPIResourceList struct { Count int `json:"count"` diff --git a/internal/pokeclient/pokeclient.go b/internal/pokeclient/pokeclient.go index 83d1d9d..936ca40 100644 --- a/internal/pokeclient/pokeclient.go +++ b/internal/pokeclient/pokeclient.go @@ -12,6 +12,12 @@ import ( "codeflow.dananglin.me.uk/apollo/pokedex/internal/pokecache" ) +const ( + baseURL string = "https://pokeapi.co/api/v2" + + LocationAreaEndpoint = baseURL + "/location-area" +) + type Client struct { httpClient http.Client cache *pokecache.Cache @@ -38,7 +44,7 @@ func (c *Client) GetNamedAPIResourceList(url string) (pokeapi.NamedAPIResourceLi fmt.Println("Using data from cache.") 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 @@ -84,6 +90,62 @@ func (c *Client) GetNamedAPIResourceList(url string) (pokeapi.NamedAPIResourceLi 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 { if err := json.Unmarshal(data, value); err != nil { return fmt.Errorf("unable to decode the JSON data: %w", err) diff --git a/main.go b/main.go index 14ffe4e..fcf0dc8 100644 --- a/main.go +++ b/main.go @@ -2,20 +2,17 @@ package main import ( "bufio" + "errors" "fmt" "maps" "os" "slices" + "strings" "time" "codeflow.dananglin.me.uk/apollo/pokedex/internal/pokeclient" ) -const ( - baseURL string = "https://pokeapi.co/api/v2" - locationAreaEndpoint string = "/location-area" -) - type State struct { Previous *string Next *string @@ -29,7 +26,7 @@ type command struct { callback callbackFunc } -type callbackFunc func() error +type callbackFunc func(args []string) error func main() { run() @@ -45,7 +42,7 @@ func run() { "exit": { name: "exit", description: "Exit the Pokedex", - callback: commandExit, + callback: exitFunc, }, "help": { name: "help", @@ -55,12 +52,17 @@ func run() { "map": { name: "map", description: "Displays the next 20 locations in the Pokemon world", - callback: commandMap(client), + callback: mapFunc(client), }, "mapb": { name: "map back", 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{ name: "help", description: "Displays a help message", - callback: commandHelp(summaries), + callback: helpFunc(summaries), } fmt.Printf("\nWelcome to the Pokedex!\n") @@ -78,7 +80,9 @@ func run() { scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { - command := scanner.Text() + input := scanner.Text() + + command, args := parseArgs(input) cmd, ok := commandMap[command] if !ok { @@ -97,7 +101,7 @@ func run() { continue } - if err := commandMap[command].callback(); err != nil { + if err := commandMap[command].callback(args); err != nil { fmt.Printf("ERROR: %v.\n", err) } @@ -105,8 +109,8 @@ func run() { } } -func commandHelp(summaries map[string]string) callbackFunc { - return func() error { +func helpFunc(summaries map[string]string) callbackFunc { + return func(_ []string) error { keys := []string{} 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) return nil } -func commandMap(client *pokeclient.Client) callbackFunc { - return func() error { +func mapFunc(client *pokeclient.Client) callbackFunc { + return func(_ []string) error { url := state.Next if url == nil { url = new(string) - *url = baseURL + locationAreaEndpoint + *url = pokeclient.LocationAreaEndpoint } return printResourceList(client, *url) } } -func commandMapB(client *pokeclient.Client) callbackFunc { - return func() error { +func mapBFunc(client *pokeclient.Client) callbackFunc { + return func(_ []string) error { url := state.Previous if url == nil { 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 { list, err := client.GetNamedAPIResourceList(url) if err != nil { @@ -181,3 +220,17 @@ func summaryMap(commandMap map[string]command) map[string]string { 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:] +}