From a51306d8a8ca35b63599ff1f6971a87ea5e98d2e Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Wed, 3 Dec 2025 11:08:53 -0800 Subject: [PATCH 01/14] adding status code check to API call (#206) --- connections/connection.go | 11 ++++++++--- connections/connection_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/connections/connection.go b/connections/connection.go index fa509f9..8d05e0a 100644 --- a/connections/connection.go +++ b/connections/connection.go @@ -36,12 +36,17 @@ func ApiCallSetup(rawURL string, target interface{}, skipHTTPSCheck bool) error Timeout: time.Second * 30, } - res, err := client.Get(parsedURL.String()) + resp, err := client.Get(parsedURL.String()) if err != nil { return fmt.Errorf("error making GET request: %w", err) } - defer res.Body.Close() - body, err := io.ReadAll(res.Body) + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("non-200 response: %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("error reading response body: %w", err) } diff --git a/connections/connection_test.go b/connections/connection_test.go index 29f1e70..d5fb136 100644 --- a/connections/connection_test.go +++ b/connections/connection_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "net/http" "net/http/httptest" + "strconv" "testing" "github.com/digitalghost-dev/poke-cli/structs" @@ -57,6 +58,33 @@ func TestApiCallSetup(t *testing.T) { require.Error(t, err) assert.Contains(t, err.Error(), "error unmarshalling JSON") }) + + t.Run("non-200 status code returns error", func(t *testing.T) { + testCases := []struct { + name string + statusCode int + }{ + {"404 Not Found", http.StatusNotFound}, + {"500 Internal Server Error", http.StatusInternalServerError}, + {"403 Forbidden", http.StatusForbidden}, + {"503 Service Unavailable", http.StatusServiceUnavailable}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(tc.statusCode) + })) + defer ts.Close() + + var target map[string]string + err := ApiCallSetup(ts.URL, &target, true) + require.Error(t, err) + assert.Contains(t, err.Error(), "non-200 response") + assert.Contains(t, err.Error(), strconv.Itoa(tc.statusCode)) + }) + } + }) } func TestAbilityApiCall(t *testing.T) { From 5dbb222d69be855ce8f34aeb7687c029ddeecd2d Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Wed, 3 Dec 2025 11:24:07 -0800 Subject: [PATCH 02/14] replacing anon `YellowAdaptive` function (#207) --- cmd/speed/speed.go | 31 ++++++++++++++++--------------- styling/styling.go | 39 +++++++++++++++++++-------------------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/cmd/speed/speed.go b/cmd/speed/speed.go index 0617d51..48928f9 100644 --- a/cmd/speed/speed.go +++ b/cmd/speed/speed.go @@ -4,6 +4,12 @@ import ( "errors" "flag" "fmt" + "log" + "math" + "os" + "strconv" + "strings" + "github.com/charmbracelet/huh" "github.com/charmbracelet/lipgloss" xstrings "github.com/charmbracelet/x/exp/strings" @@ -12,11 +18,6 @@ import ( "github.com/digitalghost-dev/poke-cli/styling" "golang.org/x/text/cases" "golang.org/x/text/language" - "log" - "math" - "os" - "strconv" - "strings" ) // DefaultSpeedStat is the default implementation of SpeedStatFunc @@ -304,18 +305,18 @@ func formula() (string, error) { finalSpeedStr := fmt.Sprintf("%.0f", finalSpeedFloor) header := fmt.Sprintf("%s at level %s with selected options has a current speed of %s.", - styling.YellowAdaptive(chosenPokemon), - styling.YellowAdaptive(pokemon.Level), - styling.YellowAdaptive(finalSpeedStr), + styling.Yellow.Render(chosenPokemon), + styling.Yellow.Render(pokemon.Level), + styling.Yellow.Render(finalSpeedStr), ) body := fmt.Sprintf("EVs: %s\nIVs: %s\nModifiers: %s\nNature: %s\nAbility: %s\nSpeed Stage: %s\nBase Speed: %s", - styling.YellowAdaptive(pokemon.SpeedEV), - styling.YellowAdaptive(pokemon.SpeedIV), - styling.YellowAdaptive(xstrings.EnglishJoin(pokemon.Modifier, true)), - styling.YellowAdaptive(pokemon.Nature), - styling.YellowAdaptive(pokemon.Ability), - styling.YellowAdaptive(pokemon.SpeedStage), - styling.YellowAdaptive(speedStr), + styling.Yellow.Render(pokemon.SpeedEV), + styling.Yellow.Render(pokemon.SpeedIV), + styling.Yellow.Render(xstrings.EnglishJoin(pokemon.Modifier, true)), + styling.Yellow.Render(pokemon.Nature), + styling.Yellow.Render(pokemon.Ability), + styling.Yellow.Render(pokemon.SpeedStage), + styling.Yellow.Render(speedStr), ) docStyle := lipgloss.NewStyle(). diff --git a/styling/styling.go b/styling/styling.go index 8c43ac3..d4a65a1 100644 --- a/styling/styling.go +++ b/styling/styling.go @@ -2,46 +2,45 @@ package styling import ( "fmt" - "github.com/charmbracelet/huh" - "github.com/charmbracelet/lipgloss" "image/color" "regexp" + + "github.com/charmbracelet/huh" + "github.com/charmbracelet/lipgloss" ) var ( - Green = lipgloss.NewStyle().Foreground(lipgloss.Color("#38B000")) - Red = lipgloss.NewStyle().Foreground(lipgloss.Color("#D00000")) - Gray = lipgloss.Color("#777777") - YellowAdaptive = func(s string) string { - return lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#E1AD01", Dark: "#FFDE00"}).Render(s) - } + Green = lipgloss.NewStyle().Foreground(lipgloss.Color("#38B000")) + Red = lipgloss.NewStyle().Foreground(lipgloss.Color("#D00000")) + Gray = lipgloss.Color("#777777") + Yellow = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#E1AD01", Dark: "#FFDE00"}) ColoredBullet = lipgloss.NewStyle(). - SetString("•"). - Foreground(lipgloss.Color("#FFCC00")) + SetString("•"). + Foreground(lipgloss.Color("#FFCC00")) CheckboxStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFCC00")) KeyMenu = lipgloss.NewStyle().Foreground(lipgloss.Color("#777777")) DocsLink = lipgloss.NewStyle(). - Foreground(lipgloss.AdaptiveColor{Light: "#E1AD01", Dark: "#FFCC00"}). - Render("\x1b]8;;https://docs.poke-cli.com\x1b\\docs.poke-cli.com\x1b]8;;\x1b\\") + Foreground(lipgloss.AdaptiveColor{Light: "#E1AD01", Dark: "#FFCC00"}). + Render("\x1b]8;;https://docs.poke-cli.com\x1b\\docs.poke-cli.com\x1b]8;;\x1b\\") StyleBold = lipgloss.NewStyle().Bold(true) StyleItalic = lipgloss.NewStyle().Italic(true) StyleUnderline = lipgloss.NewStyle().Underline(true) HelpBorder = lipgloss.NewStyle(). - BorderStyle(lipgloss.RoundedBorder()). - BorderForeground(lipgloss.Color("#FFCC00")) + BorderStyle(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("#FFCC00")) ErrorColor = lipgloss.NewStyle().Foreground(lipgloss.Color("#F2055C")) ErrorBorder = lipgloss.NewStyle(). - BorderStyle(lipgloss.RoundedBorder()). - BorderForeground(lipgloss.Color("#F2055C")) + BorderStyle(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("#F2055C")) WarningColor = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF8C00")) WarningBorder = lipgloss.NewStyle(). - BorderStyle(lipgloss.RoundedBorder()). - BorderForeground(lipgloss.Color("#FF8C00")) + BorderStyle(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("#FF8C00")) TypesTableBorder = lipgloss.NewStyle(). - BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color("#FFCC00")) + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("#FFCC00")) ColorMap = map[string]string{ "normal": "#B7B7A9", "fire": "#FF4422", From 774cb2055169889ff39d48f537ecee9283811935 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Wed, 3 Dec 2025 18:16:26 -0800 Subject: [PATCH 03/14] updating version numbers --- .github/workflows/ci.yml | 2 +- .goreleaser.yml | 2 +- Dockerfile | 2 +- README.md | 6 +++--- card_data/pipelines/poke_cli_dbt/dbt_project.yml | 2 +- nfpm.yaml | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 155c5c7..8988a2b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ on: - main env: - VERSION_NUMBER: 'v1.8.0' + VERSION_NUMBER: 'v1.8.1' DOCKERHUB_REGISTRY_NAME: 'digitalghostdev/poke-cli' AWS_REGION: 'us-west-2' diff --git a/.goreleaser.yml b/.goreleaser.yml index 16995b2..88a2427 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -14,7 +14,7 @@ builds: - windows - darwin ldflags: - - -s -w -X main.version=v1.8.0 + - -s -w -X main.version=v1.8.1 archives: - formats: [ 'zip' ] diff --git a/Dockerfile b/Dockerfile index 7605a0c..327d079 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ RUN go mod download COPY . . -RUN go build -ldflags "-X main.version=v1.8.0" -o poke-cli . +RUN go build -ldflags "-X main.version=v1.8.1" -o poke-cli . # build 2 FROM --platform=$BUILDPLATFORM alpine:3.22 diff --git a/README.md b/README.md index fb4e1e7..92a5b9b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ pokemon-logo

Pokémon CLI

version-label - docker-image-size + docker-image-size ci-status-badge
@@ -95,11 +95,11 @@ Cloudsmith is a fully cloud-based service that lets you easily create, store, an 3. Choose how to interact with the container: * Run a single command and exit: ```bash - docker run --rm -it digitalghostdev/poke-cli:v1.8.0 [subcommand] flag] + docker run --rm -it digitalghostdev/poke-cli:v1.8.1 [subcommand] flag] ``` * Enter the container and use its shell: ```bash - docker run --rm -it --name poke-cli --entrypoint /bin/sh digitalghostdev/poke-cli:v1.8.0 -c "cd /app && exec sh" + docker run --rm -it --name poke-cli --entrypoint /bin/sh digitalghostdev/poke-cli:v1.8.1 -c "cd /app && exec sh" # placed into the /app directory, run the program with './poke-cli' # example: ./poke-cli ability swift-swim ``` diff --git a/card_data/pipelines/poke_cli_dbt/dbt_project.yml b/card_data/pipelines/poke_cli_dbt/dbt_project.yml index 06c038e..333cfe3 100644 --- a/card_data/pipelines/poke_cli_dbt/dbt_project.yml +++ b/card_data/pipelines/poke_cli_dbt/dbt_project.yml @@ -1,5 +1,5 @@ name: 'poke_cli_dbt' -version: '1.8.0' +version: '1.8.1' profile: 'poke_cli_dbt' diff --git a/nfpm.yaml b/nfpm.yaml index dca2fa6..dfea760 100644 --- a/nfpm.yaml +++ b/nfpm.yaml @@ -1,7 +1,7 @@ name: "poke-cli" arch: "arm64" platform: "linux" -version: "v1.8.0" +version: "v1.8.1" section: "default" version_schema: semver maintainer: "Christian S" From eb3ba2d5b453d097ca460c55c23c19a727b8310a Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Thu, 4 Dec 2025 14:46:19 -0800 Subject: [PATCH 04/14] adding data infrastructure diagram and steps --- card_data/README.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/card_data/README.md b/card_data/README.md index fe89ea1..9017a98 100644 --- a/card_data/README.md +++ b/card_data/README.md @@ -3,4 +3,23 @@ This directory stores all the code for all backend data processing related to Pokémon TCG data. Instead of calling directly to the PokéAPI for data from the video game, I took this a step further -and decided to process all the data myself, load it into Supabase, and read from that API. \ No newline at end of file +and decided to process all the data myself, load it into Supabase, and read from that API. + +## Data Architecture +Runs at 2:00PM PST daily. +![data_diagram](https://poke-cli-s3-bucket.s3.us-west-2.amazonaws.com/data_infrastructure.png) + + +1. TCGPlayer pricing data and TCGDex card data are called and processed through a data pipeline orchestrated by Dagster and hosted on AWS. + +2. When the pipeline starts, Pydantic validates the incoming API data against a pre-defined schema, ensuring the data types match the expected structure. + +3. Polars is used to create DataFrames. + +4. The data is loaded into a Supabase staging schema. + +5. Soda data quality checks are performed. + +6. `dbt` runs and builds the final tables in a Supabase production schema. + +7. Users are then able to query the `pokeapi.co` or supabase APIs for either video game or trading card data, respectively. From 8459805f1a8e414089027d1b30bd699cf52ad86b Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Fri, 5 Dec 2025 21:30:09 -0800 Subject: [PATCH 05/14] adding generic helper function and interface to reduce API caller duplication (#208) --- cmd/pokemon/pokemon.go | 2 +- connections/connection.go | 129 ++++++++++---------------------------- structs/structs.go | 24 +++++++ 3 files changed, 59 insertions(+), 96 deletions(-) diff --git a/cmd/pokemon/pokemon.go b/cmd/pokemon/pokemon.go index e67c112..8bf8f91 100644 --- a/cmd/pokemon/pokemon.go +++ b/cmd/pokemon/pokemon.go @@ -77,7 +77,7 @@ func PokemonCommand() (string, error) { return output.String(), err } - pokemonSpeciesStruct, err := connections.PokemonSpeciesApiCall("pokemon-species", pokemonStruct.Species.Name, connections.APIURL) + pokemonSpeciesStruct, _, err := connections.PokemonSpeciesApiCall("pokemon-species", pokemonStruct.Species.Name, connections.APIURL) if err != nil { output.WriteString(err.Error()) return output.String(), err diff --git a/connections/connection.go b/connections/connection.go index 8d05e0a..cbe9c28 100644 --- a/connections/connection.go +++ b/connections/connection.go @@ -16,6 +16,30 @@ import ( const APIURL = "https://pokeapi.co/api/v2/" +var httpClient = &http.Client{Timeout: 30 * time.Second} + +type EndpointResource interface { + GetResourceName() string +} + +func FetchEndpoint[T EndpointResource](endpoint, resourceName, baseURL, resourceType string) (T, string, error) { + var zero T + fullURL := baseURL + endpoint + "/" + resourceName + + var result T + err := ApiCallSetup(fullURL, &result, false) + + if err != nil { + errMessage := styling.ErrorBorder.Render( + styling.ErrorColor.Render("✖ Error!"), + fmt.Sprintf("\n%s not found.\n• Perhaps a typo?\n• Missing a hyphen instead of a space?", resourceType), + ) + return zero, "", fmt.Errorf("%s", errMessage) + } + + return result, result.GetResourceName(), nil +} + // ApiCallSetup Helper function to handle API calls and JSON unmarshalling func ApiCallSetup(rawURL string, target interface{}, skipHTTPSCheck bool) error { parsedURL, err := url.Parse(rawURL) @@ -32,11 +56,7 @@ func ApiCallSetup(rawURL string, target interface{}, skipHTTPSCheck bool) error return errors.New("only HTTPS URLs are allowed for security reasons") } - client := http.Client{ - Timeout: time.Second * 30, - } - - resp, err := client.Get(parsedURL.String()) + resp, err := httpClient.Get(parsedURL.String()) if err != nil { return fmt.Errorf("error making GET request: %w", err) } @@ -59,107 +79,26 @@ func ApiCallSetup(rawURL string, target interface{}, skipHTTPSCheck bool) error return nil } -// AbilityApiCall function for calling the ability endpoint of the pokeAPI -func AbilityApiCall(endpoint string, abilityName string, baseURL string) (structs.AbilityJSONStruct, string, error) { - fullURL := baseURL + endpoint + "/" + abilityName - - var abilityStruct structs.AbilityJSONStruct - err := ApiCallSetup(fullURL, &abilityStruct, false) - - if err != nil { - errMessage := styling.ErrorBorder.Render( - styling.ErrorColor.Render("✖ Error!"), - "\nAbility not found.\n\u2022 Perhaps a typo?\n\u2022 Missing a hyphen instead of a space?", - ) - return structs.AbilityJSONStruct{}, "", fmt.Errorf("%s", errMessage) - } - - return abilityStruct, abilityStruct.Name, nil +func AbilityApiCall(endpoint, abilityName, baseURL string) (structs.AbilityJSONStruct, string, error) { + return FetchEndpoint[structs.AbilityJSONStruct](endpoint, abilityName, baseURL, "Ability") } -// ItemApiCall function for calling the item endpoint of the pokeAPI func ItemApiCall(endpoint string, itemName string, baseURL string) (structs.ItemJSONStruct, string, error) { - fullURL := baseURL + endpoint + "/" + itemName - - var itemStruct structs.ItemJSONStruct - err := ApiCallSetup(fullURL, &itemStruct, false) - - if err != nil { - errMessage := styling.ErrorBorder.Render( - styling.ErrorColor.Render("✖ Error!"), - "\nItem not found.\n\u2022 Perhaps a typo?\n\u2022 Missing a hyphen instead of a space?", - ) - return structs.ItemJSONStruct{}, "", fmt.Errorf("%s", errMessage) - } - - return itemStruct, itemStruct.Name, nil + return FetchEndpoint[structs.ItemJSONStruct](endpoint, itemName, baseURL, "Item") } -// MoveApiCall function for calling the move endpoint of the pokeAPI func MoveApiCall(endpoint string, moveName string, baseURL string) (structs.MoveJSONStruct, string, error) { - fullURL := baseURL + endpoint + "/" + moveName - - var moveStruct structs.MoveJSONStruct - err := ApiCallSetup(fullURL, &moveStruct, false) - - if err != nil { - errMessage := styling.ErrorBorder.Render( - styling.ErrorColor.Render("✖ Error!"), - "\nMove not found.\n\u2022 Perhaps a typo?\n\u2022 Missing a hyphen instead of a space?", - ) - return structs.MoveJSONStruct{}, "", fmt.Errorf("%s", errMessage) - } - - return moveStruct, moveStruct.Name, nil + return FetchEndpoint[structs.MoveJSONStruct](endpoint, moveName, baseURL, "Move") } -// PokemonApiCall function for calling the pokemon endpoint of the pokeAPI func PokemonApiCall(endpoint string, pokemonName string, baseURL string) (structs.PokemonJSONStruct, string, error) { - fullURL := baseURL + endpoint + "/" + pokemonName - - var pokemonStruct structs.PokemonJSONStruct - err := ApiCallSetup(fullURL, &pokemonStruct, false) - - if err != nil { - errMessage := styling.ErrorBorder.Render( - styling.ErrorColor.Render("✖ Error!"), - "\nPokémon not found.\n\u2022 Perhaps a typo?\n\u2022 Missing a hyphen instead of a space?", - ) - return structs.PokemonJSONStruct{}, "", fmt.Errorf("%s", errMessage) - } - - return pokemonStruct, pokemonStruct.Name, nil + return FetchEndpoint[structs.PokemonJSONStruct](endpoint, pokemonName, baseURL, "Pokémon") } -// PokemonSpeciesApiCall function for calling the pokemon endpoint of the pokeAPI -func PokemonSpeciesApiCall(endpoint string, pokemonSpeciesName string, baseURL string) (structs.PokemonSpeciesJSONStruct, error) { - fullURL := baseURL + endpoint + "/" + pokemonSpeciesName - - var pokemonSpeciesStruct structs.PokemonSpeciesJSONStruct - err := ApiCallSetup(fullURL, &pokemonSpeciesStruct, false) - - if err != nil { - errMessage := styling.ErrorBorder.Render( - styling.ErrorColor.Render("✖ Error!"), - "\nPokémon not found.\n\u2022 Perhaps a typo?\n\u2022 Missing a hyphen instead of a space?", - ) - return structs.PokemonSpeciesJSONStruct{}, fmt.Errorf("%s", errMessage) - } - - return pokemonSpeciesStruct, nil +func PokemonSpeciesApiCall(endpoint string, pokemonSpeciesName string, baseURL string) (structs.PokemonSpeciesJSONStruct, string, error) { + return FetchEndpoint[structs.PokemonSpeciesJSONStruct](endpoint, pokemonSpeciesName, baseURL, "PokémonSpecies") } -// TypesApiCall function for calling the type endpoint of the pokeAPI -func TypesApiCall(endpoint string, typesName string, baseURL string) (structs.TypesJSONStruct, string, int) { - fullURL := baseURL + endpoint + "/" + typesName - var typesStruct structs.TypesJSONStruct - - err := ApiCallSetup(fullURL, &typesStruct, false) - - if err != nil { - fmt.Println(err) - return structs.TypesJSONStruct{}, "", 0 - } - - return typesStruct, typesStruct.Name, typesStruct.ID +func TypesApiCall(endpoint string, typesName string, baseURL string) (structs.TypesJSONStruct, string, error) { + return FetchEndpoint[structs.TypesJSONStruct](endpoint, typesName, baseURL, "Type") } diff --git a/structs/structs.go b/structs/structs.go index 90a4e2c..4381edf 100644 --- a/structs/structs.go +++ b/structs/structs.go @@ -1,5 +1,29 @@ package structs +func (a AbilityJSONStruct) GetResourceName() string { + return a.Name +} + +func (i ItemJSONStruct) GetResourceName() string { + return i.Name +} + +func (m MoveJSONStruct) GetResourceName() string { + return m.Name +} + +func (p PokemonJSONStruct) GetResourceName() string { + return p.Name +} + +func (s PokemonSpeciesJSONStruct) GetResourceName() string { + return s.Name +} + +func (t TypesJSONStruct) GetResourceName() string { + return t.Name +} + // AbilityJSONStruct ability endpoint from API type AbilityJSONStruct struct { Name string `json:"name"` From 5fdf2187bd8439f87ba0691c1412ee2b08e4f0a3 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Fri, 5 Dec 2025 21:33:37 -0800 Subject: [PATCH 06/14] updating tests --- cmd/ability/ability_test.go | 22 +++---- connections/connection_test.go | 95 +++++++++++++++++----------- testdata/ability_poison_point.golden | 3 + testdata/main_latest_flag.golden | 2 +- 4 files changed, 71 insertions(+), 51 deletions(-) create mode 100644 testdata/ability_poison_point.golden diff --git a/cmd/ability/ability_test.go b/cmd/ability/ability_test.go index 5da9108..020d6c0 100644 --- a/cmd/ability/ability_test.go +++ b/cmd/ability/ability_test.go @@ -1,26 +1,15 @@ package ability import ( + "os" + "testing" + "github.com/digitalghost-dev/poke-cli/cmd/utils" "github.com/digitalghost-dev/poke-cli/styling" "github.com/stretchr/testify/assert" - "os" - "testing" ) func TestAbilityCommand(t *testing.T) { - err := os.Setenv("GO_TESTING", "1") - if err != nil { - t.Fatalf("Failed to set GO_TESTING env var: %v", err) - } - - defer func() { - err := os.Unsetenv("GO_TESTING") - if err != nil { - t.Logf("Warning: failed to unset GO_TESTING: %v", err) - } - }() - tests := []struct { name string args []string @@ -53,6 +42,11 @@ func TestAbilityCommand(t *testing.T) { args: []string{"ability", "anger-point", "--pokemon"}, expectedOutput: utils.LoadGolden(t, "ability_flag_pokemon.golden"), }, + { + name: "Ability command: special character in API call", + args: []string{"ability", "poison-point"}, + expectedOutput: utils.LoadGolden(t, "ability_poison_point.golden"), + }, } for _, tt := range tests { diff --git a/connections/connection_test.go b/connections/connection_test.go index d5fb136..0a495ad 100644 --- a/connections/connection_test.go +++ b/connections/connection_test.go @@ -114,10 +114,11 @@ func TestAbilityApiCall(t *testing.T) { })) defer ts.Close() - ability, _, err := AbilityApiCall("/ability", "non-existent-ability", ts.URL) + ability, name, err := AbilityApiCall("/ability", "non-existent-ability", ts.URL) require.Error(t, err, "Expected an error for invalid ability") assert.Equal(t, structs.AbilityJSONStruct{}, ability, "Expected empty ability struct on error") + assert.Equal(t, "", name, "Expected empty name string on error") assert.Contains(t, err.Error(), "Ability not found", "Expected 'Ability not found' in error message") assert.Contains(t, err.Error(), "Perhaps a typo?", "Expected helpful suggestion in error message") @@ -151,10 +152,11 @@ func TestItemApiCall(t *testing.T) { })) defer ts.Close() - item, _, err := ItemApiCall("/item", "non-existent-item", ts.URL) + item, name, err := ItemApiCall("/item", "non-existent-item", ts.URL) require.Error(t, err, "Expected an error for invalid item") assert.Equal(t, structs.ItemJSONStruct{}, item, "Expected empty item struct on error") + assert.Equal(t, "", name, "Expected empty name string on error") assert.Contains(t, err.Error(), "Item not found", "Expected 'Item not found' in error message") assert.Contains(t, err.Error(), "Perhaps a typo?", "Expected helpful suggestion in error message") @@ -188,10 +190,11 @@ func TestMoveApiCall(t *testing.T) { })) defer ts.Close() - move, _, err := MoveApiCall("/move", "non-existent-move", ts.URL) + move, name, err := MoveApiCall("/move", "non-existent-move", ts.URL) require.Error(t, err, "Expected an error for invalid move") assert.Equal(t, structs.MoveJSONStruct{}, move, "Expected empty move struct on error") + assert.Equal(t, "", name, "Expected empty name string on error") assert.Contains(t, err.Error(), "Move not found", "Expected 'Move not found' in error message") assert.Contains(t, err.Error(), "Perhaps a typo?", "Expected helpful suggestion in error message") @@ -225,10 +228,11 @@ func TestPokemonApiCall(t *testing.T) { })) defer ts.Close() - pokemon, _, err := PokemonApiCall("/pokemon", "non-existent-pokemon", ts.URL) + pokemon, name, err := PokemonApiCall("/pokemon", "non-existent-pokemon", ts.URL) require.Error(t, err, "Expected an error for invalid pokemon") assert.Equal(t, structs.PokemonJSONStruct{}, pokemon, "Expected empty pokemon struct on error") + assert.Equal(t, "", name, "Expected empty name string on error") assert.Contains(t, err.Error(), "Pokémon not found", "Expected 'Pokémon not found' in error message") assert.Contains(t, err.Error(), "Perhaps a typo?", "Expected helpful suggestion in error message") @@ -237,40 +241,58 @@ func TestPokemonApiCall(t *testing.T) { // TestTypesApiCall - Test for the TypesApiCall function func TestTypesApiCall(t *testing.T) { - expectedTypes := structs.TypesJSONStruct{ - Name: "electric", - ID: 13, - Pokemon: []struct { - Pokemon struct { - Name string `json:"name"` - URL string `json:"url"` - } `json:"pokemon"` - Slot int `json:"slot"` - }{ - {Pokemon: struct { - Name string `json:"name"` - URL string `json:"url"` - }{Name: "pikachu", URL: "https://pokeapi.co/api/v2/pokemon/25/"}, - Slot: 1}, - }, - } + t.Run("Successful API call returns expected type", func(t *testing.T) { + expectedTypes := structs.TypesJSONStruct{ + Name: "electric", + ID: 13, + Pokemon: []struct { + Pokemon struct { + Name string `json:"name"` + URL string `json:"url"` + } `json:"pokemon"` + Slot int `json:"slot"` + }{ + {Pokemon: struct { + Name string `json:"name"` + URL string `json:"url"` + }{Name: "pikachu", URL: "https://pokeapi.co/api/v2/pokemon/25/"}, + Slot: 1}, + }, + } - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - err := json.NewEncoder(w).Encode(expectedTypes) - assert.NoError(t, err, "Expected no error for skipHTTPSCheck") - })) - defer ts.Close() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(expectedTypes) + assert.NoError(t, err, "Expected no error for encoding response") + })) + defer ts.Close() + + typesStruct, name, err := TypesApiCall("/type", "electric", ts.URL) + + require.NoError(t, err, "Expected no error on successful API call") + assert.Equal(t, expectedTypes, typesStruct, "Expected types struct does not match") + assert.Equal(t, "electric", name, "Expected type name does not match") + }) - typesStruct, name, id := TypesApiCall("/type", "electric", ts.URL) + t.Run("Failed API call returns styled error", func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Simulate API failure (e.g., 404 Not Found) + http.Error(w, "Not Found", http.StatusNotFound) + })) + defer ts.Close() + + typesStruct, name, err := TypesApiCall("/type", "non-existent-type", ts.URL) + + require.Error(t, err, "Expected an error for invalid type") + assert.Equal(t, structs.TypesJSONStruct{}, typesStruct, "Expected empty types struct on error") + assert.Equal(t, "", name, "Expected empty name string on error") - assert.Equal(t, expectedTypes, typesStruct) - assert.Equal(t, "electric", name) - assert.Equal(t, 13, id) + assert.Contains(t, err.Error(), "Type not found", "Expected 'Type not found' in error message") + assert.Contains(t, err.Error(), "Perhaps a typo?", "Expected helpful suggestion in error message") + }) } func TestPokemonSpeciesApiCall(t *testing.T) { - // Successful API call returns expected species data t.Run("Successful API call returns expected species", func(t *testing.T) { expectedSpecies := structs.PokemonSpeciesJSONStruct{ Name: "flareon", @@ -283,13 +305,13 @@ func TestPokemonSpeciesApiCall(t *testing.T) { })) defer ts.Close() - species, err := PokemonSpeciesApiCall("/pokemon-species", "flareon", ts.URL) + species, name, err := PokemonSpeciesApiCall("/pokemon-species", "flareon", ts.URL) require.NoError(t, err, "Expected no error on successful API call") assert.Equal(t, expectedSpecies, species, "Expected species struct does not match") + assert.Equal(t, "flareon", name, "Expected species name does not match") }) - // Failed API call returns styled error t.Run("Failed API call returns styled error", func(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Simulate API failure (e.g., 404 Not Found) @@ -297,12 +319,13 @@ func TestPokemonSpeciesApiCall(t *testing.T) { })) defer ts.Close() - species, err := PokemonSpeciesApiCall("/pokemon-species", "non-existent-species", ts.URL) + species, name, err := PokemonSpeciesApiCall("/pokemon-species", "non-existent-species", ts.URL) require.Error(t, err, "Expected an error for invalid species") assert.Equal(t, structs.PokemonSpeciesJSONStruct{}, species, "Expected empty species struct on error") + assert.Equal(t, "", name, "Expected empty name string on error") - assert.Contains(t, err.Error(), "Pokémon not found", "Expected 'Pokémon not found' in error message") + assert.Contains(t, err.Error(), "PokémonSpecies not found", "Expected 'PokémonSpecies not found' in error message") assert.Contains(t, err.Error(), "Perhaps a typo?", "Expected helpful suggestion in error message") }) } diff --git a/testdata/ability_poison_point.golden b/testdata/ability_poison_point.golden new file mode 100644 index 0000000..58444f9 --- /dev/null +++ b/testdata/ability_poison_point.golden @@ -0,0 +1,3 @@ +Poison Point +• First introduced in generation III +• Effect: Has a 30% chance of poisoning attacking Pokémon on contact. \ No newline at end of file diff --git a/testdata/main_latest_flag.golden b/testdata/main_latest_flag.golden index 4166dd4..ebd0a2a 100644 --- a/testdata/main_latest_flag.golden +++ b/testdata/main_latest_flag.golden @@ -2,6 +2,6 @@ ┃ ┃ ┃ Latest available release ┃ ┃ on GitHub: ┃ -┃ • v1.7.4 ┃ +┃ • v1.8.0 ┃ ┃ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ From 0b05b6123518f61b9f9ac169b20a69c5a18c7ca8 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Sat, 6 Dec 2025 10:33:25 -0800 Subject: [PATCH 07/14] fixing printing issue with '%' (#205) --- cmd/ability/ability.go | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/cmd/ability/ability.go b/cmd/ability/ability.go index 4a5cc30..eda5795 100644 --- a/cmd/ability/ability.go +++ b/cmd/ability/ability.go @@ -3,14 +3,15 @@ package ability import ( "flag" "fmt" + "os" + "strings" + "github.com/digitalghost-dev/poke-cli/cmd/utils" "github.com/digitalghost-dev/poke-cli/connections" "github.com/digitalghost-dev/poke-cli/flags" "github.com/digitalghost-dev/poke-cli/styling" "golang.org/x/text/cases" "golang.org/x/text/language" - "os" - "strings" ) func AbilityCommand() (string, error) { @@ -52,18 +53,12 @@ func AbilityCommand() (string, error) { if err := abilityFlags.Parse(args[3:]); err != nil { output.WriteString(fmt.Sprintf("error parsing flags: %v\n", err)) abilityFlags.Usage() - if os.Getenv("GO_TESTING") != "1" { - os.Exit(1) - } + return output.String(), err } abilitiesStruct, abilityName, err := connections.AbilityApiCall(endpoint, abilityName, connections.APIURL) if err != nil { - if os.Getenv("GO_TESTING") != "1" { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } return err.Error(), nil } @@ -88,7 +83,6 @@ func AbilityCommand() (string, error) { capitalizedAbility := cases.Title(language.English).String(strings.ReplaceAll(abilityName, "-", " ")) output.WriteString(styling.StyleBold.Render(capitalizedAbility) + "\n") - // Print the generation where the ability was first introduced. generationParts := strings.Split(abilitiesStruct.Generation.Name, "-") if len(generationParts) > 1 { generationUpper := strings.ToUpper(generationParts[1]) @@ -100,9 +94,9 @@ func AbilityCommand() (string, error) { // API is missing some data for the short_effect for abilities from Generation 9. // If short_effect is empty, fallback to the move's flavor_text_entry. if englishShortEffect == "" { - output.WriteString(fmt.Sprintf("%s Effect: "+englishFlavorEntry, styling.ColoredBullet)) + output.WriteString(fmt.Sprintf("%s Effect: %s", styling.ColoredBullet, englishFlavorEntry)) } else { - output.WriteString(fmt.Sprintf("%s Effect: "+englishShortEffect, styling.ColoredBullet)) + output.WriteString(fmt.Sprintf("%s Effect: %s", styling.ColoredBullet, englishShortEffect)) } if *pokemonFlag || *shortPokemonFlag { From 8c515f795112b8f4221f86d1b433c0b83843e6eb Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Sat, 6 Dec 2025 10:35:19 -0800 Subject: [PATCH 08/14] adding loading screen with spinner (#204) --- cmd/card/imageviewer.go | 56 ++++++++++++-- cmd/card/imageviewer_test.go | 138 +++++++++++++++++------------------ 2 files changed, 114 insertions(+), 80 deletions(-) diff --git a/cmd/card/imageviewer.go b/cmd/card/imageviewer.go index e8f0084..670bffe 100644 --- a/cmd/card/imageviewer.go +++ b/cmd/card/imageviewer.go @@ -1,21 +1,53 @@ package card import ( + "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/digitalghost-dev/poke-cli/styling" ) type ImageModel struct { - CardName string - ImageURL string - Error error + CardName string + ImageURL string + Error error + Loading bool + Spinner spinner.Model + ImageData string +} + +type imageReadyMsg struct { + sixelData string +} + +// fetchImageCmd downloads and renders the image asynchronously +func fetchImageCmd(imageURL string) tea.Cmd { + return func() tea.Msg { + sixelData, err := CardImage(imageURL) + if err != nil { + return imageReadyMsg{err.Error()} + } + return imageReadyMsg{sixelData: sixelData} + } } func (m ImageModel) Init() tea.Cmd { - return nil + return tea.Batch( + m.Spinner.Tick, + fetchImageCmd(m.ImageURL), + ) } func (m ImageModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { + case imageReadyMsg: + m.Loading = false + m.ImageData = msg.sixelData + return m, nil + case spinner.TickMsg: + var cmd tea.Cmd + m.Spinner, cmd = m.Spinner.Update(msg) + return m, cmd case tea.KeyMsg: switch msg.String() { case "ctrl+c", "esc": @@ -26,15 +58,23 @@ func (m ImageModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m ImageModel) View() string { - return m.ImageURL + if m.Loading { + return lipgloss.NewStyle().Padding(2).Render( + m.Spinner.View() + "Loading image for \n" + m.CardName, + ) + } + return m.ImageData } func ImageRenderer(cardName string, imageURL string) ImageModel { - imageData, err := CardImage(imageURL) + s := spinner.New() + s.Spinner = spinner.Dot + s.Style = styling.Yellow return ImageModel{ CardName: cardName, - ImageURL: imageData, - Error: err, + ImageURL: imageURL, + Loading: true, + Spinner: s, } } diff --git a/cmd/card/imageviewer_test.go b/cmd/card/imageviewer_test.go index 7b2efa5..cc0361d 100644 --- a/cmd/card/imageviewer_test.go +++ b/cmd/card/imageviewer_test.go @@ -1,25 +1,18 @@ package card import ( - "image" - "image/color" - "image/png" - "net/http" - "net/http/httptest" + "strings" "testing" tea "github.com/charmbracelet/bubbletea" ) func TestImageModel_Init(t *testing.T) { - model := ImageModel{ - CardName: "001/198 - Pineco", - ImageURL: "test-sixel-data", - } + model := ImageRenderer("001/198 - Pineco", "http://example.com/image.png") cmd := model.Init() - if cmd != nil { - t.Error("Init() should return nil") + if cmd == nil { + t.Error("Init() should return a command (batch of spinner tick + fetch)") } } @@ -53,7 +46,6 @@ func TestImageModel_Update_CtrlC(t *testing.T) { msg := tea.KeyMsg{Type: tea.KeyCtrlC} _, cmd := model.Update(msg) - // Should return quit command if cmd == nil { t.Error("Update with Ctrl+C should return tea.Quit command") } @@ -73,107 +65,109 @@ func TestImageModel_Update_DifferentKey(t *testing.T) { } } -func TestImageModel_View(t *testing.T) { - expectedURL := "test-sixel-data-123" +func TestImageModel_View_Loading(t *testing.T) { + model := ImageRenderer("001/198 - Pineco", "http://example.com/image.png") + + result := model.View() + + // When loading, should show spinner and card name + if result == "" { + t.Error("View() should not be empty when loading") + } + // Can't check exact spinner output as it's dynamic, but should contain card name + if !strings.Contains(result, "001/198 - Pineco") { + t.Error("View() should contain card name when loading") + } +} + +func TestImageModel_View_Loaded(t *testing.T) { + expectedData := "test-sixel-data-123" model := ImageModel{ - CardName: "001/198 - Pineco", - ImageURL: expectedURL, + CardName: "001/198 - Pineco", + ImageURL: "http://example.com/image.png", + Loading: false, + ImageData: expectedData, } result := model.View() - if result != expectedURL { - t.Errorf("View() = %v, want %v", result, expectedURL) + if result != expectedData { + t.Errorf("View() = %v, want %v", result, expectedData) } } func TestImageModel_View_Empty(t *testing.T) { model := ImageModel{ - CardName: "001/198 - Pineco", - ImageURL: "", + CardName: "001/198 - Pineco", + ImageURL: "", + Loading: false, + ImageData: "", } result := model.View() if result != "" { - t.Errorf("View() with empty ImageURL should return empty string, got %v", result) + t.Errorf("View() with empty ImageData should return empty string, got %v", result) } } -func TestImageRenderer_Success(t *testing.T) { - // Create a test HTTP server that serves a valid PNG image - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - img := image.NewRGBA(image.Rect(0, 0, 10, 10)) - blue := color.RGBA{R: 0, G: 0, B: 255, A: 255} - for y := 0; y < 10; y++ { - for x := 0; x < 10; x++ { - img.Set(x, y, blue) - } - } - - w.Header().Set("Content-Type", "image/png") - w.WriteHeader(http.StatusOK) - _ = png.Encode(w, img) - })) - defer server.Close() - - model := ImageRenderer("Pikachu", server.URL) +func TestImageRenderer_InitializesCorrectly(t *testing.T) { + testURL := "http://example.com/pikachu.png" + model := ImageRenderer("Pikachu", testURL) if model.CardName != "Pikachu" { t.Errorf("ImageRenderer() CardName = %v, want %v", model.CardName, "Pikachu") } - if model.Error != nil { - t.Errorf("ImageRenderer() Error should be nil on success, got %v", model.Error) + if model.ImageURL != testURL { + t.Errorf("ImageRenderer() ImageURL = %v, want %v", model.ImageURL, testURL) } - if model.ImageURL == "" { - t.Error("ImageRenderer() ImageURL should not be empty on success") + if !model.Loading { + t.Error("ImageRenderer() should initialize with Loading = true") + } + + if model.ImageData != "" { + t.Error("ImageRenderer() should initialize with empty ImageData") } } -func TestImageRenderer_Error(t *testing.T) { - // Create a test HTTP server that returns an error - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNotFound) - })) - defer server.Close() +func TestImageModel_Update_ImageReady(t *testing.T) { + model := ImageRenderer("Charizard", "http://example.com/charizard.png") - model := ImageRenderer("Charizard", server.URL) + msg := imageReadyMsg{sixelData: "test-sixel-data-456"} + newModel, cmd := model.Update(msg) - if model.CardName != "Charizard" { - t.Errorf("ImageRenderer() CardName = %v, want %v", model.CardName, "Charizard") + if cmd != nil { + t.Error("Update with imageReadyMsg should return nil command") } - if model.Error == nil { - t.Error("ImageRenderer() Error should not be nil when image fetch fails") + updatedModel := newModel.(ImageModel) + if updatedModel.Loading { + t.Error(`Update with imageReadyMsg should set Loading to false`) } - if model.ImageURL != "" { - t.Errorf("ImageRenderer() ImageURL should be empty on error, got %v", model.ImageURL) + if updatedModel.ImageData != "test-sixel-data-456" { + t.Errorf("Update with imageReadyMsg should set ImageData, got %v", updatedModel.ImageData) } } -func TestImageRenderer_InvalidImage(t *testing.T) { - // Create a test HTTP server that returns invalid image data - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "image/png") - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte("not a valid image")) - })) - defer server.Close() +func TestImageModel_Update_SpinnerTick(t *testing.T) { + model := ImageRenderer("Mewtwo", "http://example.com/mewtwo.png") - model := ImageRenderer("Mewtwo", server.URL) + // Create a spinner tick message + msg := model.Spinner.Tick() - if model.CardName != "Mewtwo" { - t.Errorf("ImageRenderer() CardName = %v, want %v", model.CardName, "Mewtwo") - } + // Update should handle spinner ticks + newModel, cmd := model.Update(msg) - if model.Error == nil { - t.Error("ImageRenderer() Error should not be nil when image decoding fails") + // Should return a spinner command + if cmd == nil { + t.Error("Update with spinner.TickMsg should return a command") } - if model.ImageURL != "" { - t.Errorf("ImageRenderer() ImageURL should be empty on error, got %v", model.ImageURL) + // Model should still be ImageModel + if _, ok := newModel.(ImageModel); !ok { + t.Error("Update should return ImageModel") } } From 322dbffc22574caff137df1b63b5009fc6329888 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Sun, 7 Dec 2025 11:35:30 -0800 Subject: [PATCH 09/14] returning an error instead of image data (#210) --- cmd/card/imageviewer.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/cmd/card/imageviewer.go b/cmd/card/imageviewer.go index 670bffe..3cc85a2 100644 --- a/cmd/card/imageviewer.go +++ b/cmd/card/imageviewer.go @@ -18,6 +18,7 @@ type ImageModel struct { type imageReadyMsg struct { sixelData string + err error } // fetchImageCmd downloads and renders the image asynchronously @@ -25,7 +26,7 @@ func fetchImageCmd(imageURL string) tea.Cmd { return func() tea.Msg { sixelData, err := CardImage(imageURL) if err != nil { - return imageReadyMsg{err.Error()} + return imageReadyMsg{err: err} } return imageReadyMsg{sixelData: sixelData} } @@ -42,7 +43,13 @@ func (m ImageModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case imageReadyMsg: m.Loading = false - m.ImageData = msg.sixelData + if msg.err != nil { + m.Error = msg.err + m.ImageData = "" + } else { + m.Error = nil + m.ImageData = msg.sixelData + } return m, nil case spinner.TickMsg: var cmd tea.Cmd @@ -63,6 +70,9 @@ func (m ImageModel) View() string { m.Spinner.View() + "Loading image for \n" + m.CardName, ) } + if m.Error != nil { + return styling.Red.Render(m.Error.Error()) + } return m.ImageData } From a7bedced87345f431939bd814ce8e32c5534a188 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Sun, 7 Dec 2025 12:09:21 -0800 Subject: [PATCH 10/14] conforming function calls (#209) --- cmd/ability/ability.go | 5 +++-- cmd/item/item.go | 7 ++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/cmd/ability/ability.go b/cmd/ability/ability.go index eda5795..bbb3921 100644 --- a/cmd/ability/ability.go +++ b/cmd/ability/ability.go @@ -59,7 +59,8 @@ func AbilityCommand() (string, error) { abilitiesStruct, abilityName, err := connections.AbilityApiCall(endpoint, abilityName, connections.APIURL) if err != nil { - return err.Error(), nil + output.WriteString(err.Error()) + return output.String(), err } // Extract English short_effect @@ -102,7 +103,7 @@ func AbilityCommand() (string, error) { if *pokemonFlag || *shortPokemonFlag { if err := flags.PokemonAbilitiesFlag(&output, endpoint, abilityName); err != nil { output.WriteString(fmt.Sprintf("error parsing flags: %v\n", err)) - return "", fmt.Errorf("error parsing flags: %w", err) + return output.String(), fmt.Errorf("error parsing flags: %w", err) } } diff --git a/cmd/item/item.go b/cmd/item/item.go index 56c0978..4c5b162 100644 --- a/cmd/item/item.go +++ b/cmd/item/item.go @@ -50,11 +50,8 @@ func ItemCommand() (string, error) { itemStruct, itemName, err := connections.ItemApiCall(endpoint, itemName, connections.APIURL) if err != nil { - if os.Getenv("GO_TESTING") != "1" { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - return err.Error(), nil + output.WriteString(err.Error()) + return output.String(), err } itemInfoContainer(&output, itemStruct, itemName) From ca27efef1521fa20a06e02bcfb5679063397061e Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Sun, 7 Dec 2025 12:25:09 -0800 Subject: [PATCH 11/14] fixing linting errors --- connections/connection_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/connections/connection_test.go b/connections/connection_test.go index 0a495ad..f2fdb82 100644 --- a/connections/connection_test.go +++ b/connections/connection_test.go @@ -118,7 +118,7 @@ func TestAbilityApiCall(t *testing.T) { require.Error(t, err, "Expected an error for invalid ability") assert.Equal(t, structs.AbilityJSONStruct{}, ability, "Expected empty ability struct on error") - assert.Equal(t, "", name, "Expected empty name string on error") + assert.Empty(t, name, "Expected empty name string on error") assert.Contains(t, err.Error(), "Ability not found", "Expected 'Ability not found' in error message") assert.Contains(t, err.Error(), "Perhaps a typo?", "Expected helpful suggestion in error message") @@ -156,7 +156,7 @@ func TestItemApiCall(t *testing.T) { require.Error(t, err, "Expected an error for invalid item") assert.Equal(t, structs.ItemJSONStruct{}, item, "Expected empty item struct on error") - assert.Equal(t, "", name, "Expected empty name string on error") + assert.Empty(t, name, "Expected empty name string on error") assert.Contains(t, err.Error(), "Item not found", "Expected 'Item not found' in error message") assert.Contains(t, err.Error(), "Perhaps a typo?", "Expected helpful suggestion in error message") @@ -194,7 +194,7 @@ func TestMoveApiCall(t *testing.T) { require.Error(t, err, "Expected an error for invalid move") assert.Equal(t, structs.MoveJSONStruct{}, move, "Expected empty move struct on error") - assert.Equal(t, "", name, "Expected empty name string on error") + assert.Empty(t, name, "Expected empty name string on error") assert.Contains(t, err.Error(), "Move not found", "Expected 'Move not found' in error message") assert.Contains(t, err.Error(), "Perhaps a typo?", "Expected helpful suggestion in error message") @@ -232,7 +232,7 @@ func TestPokemonApiCall(t *testing.T) { require.Error(t, err, "Expected an error for invalid pokemon") assert.Equal(t, structs.PokemonJSONStruct{}, pokemon, "Expected empty pokemon struct on error") - assert.Equal(t, "", name, "Expected empty name string on error") + assert.Empty(t, name, "Expected empty name string on error") assert.Contains(t, err.Error(), "Pokémon not found", "Expected 'Pokémon not found' in error message") assert.Contains(t, err.Error(), "Perhaps a typo?", "Expected helpful suggestion in error message") @@ -285,7 +285,7 @@ func TestTypesApiCall(t *testing.T) { require.Error(t, err, "Expected an error for invalid type") assert.Equal(t, structs.TypesJSONStruct{}, typesStruct, "Expected empty types struct on error") - assert.Equal(t, "", name, "Expected empty name string on error") + assert.Empty(t, name, "Expected empty name string on error") assert.Contains(t, err.Error(), "Type not found", "Expected 'Type not found' in error message") assert.Contains(t, err.Error(), "Perhaps a typo?", "Expected helpful suggestion in error message") @@ -323,7 +323,7 @@ func TestPokemonSpeciesApiCall(t *testing.T) { require.Error(t, err, "Expected an error for invalid species") assert.Equal(t, structs.PokemonSpeciesJSONStruct{}, species, "Expected empty species struct on error") - assert.Equal(t, "", name, "Expected empty name string on error") + assert.Empty(t, name, "Expected empty name string on error") assert.Contains(t, err.Error(), "PokémonSpecies not found", "Expected 'PokémonSpecies not found' in error message") assert.Contains(t, err.Error(), "Perhaps a typo?", "Expected helpful suggestion in error message") From 174a990c85445fa2fdd993f4383277e8ee29f68a Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Sun, 7 Dec 2025 15:35:05 -0800 Subject: [PATCH 12/14] fixing typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 92a5b9b..5eef998 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ Cloudsmith is a fully cloud-based service that lets you easily create, store, an 3. Choose how to interact with the container: * Run a single command and exit: ```bash - docker run --rm -it digitalghostdev/poke-cli:v1.8.1 [subcommand] flag] + docker run --rm -it digitalghostdev/poke-cli:v1.8.1 [subcommand] [flag] ``` * Enter the container and use its shell: ```bash From d909560b4339992e1ecd117652afd321a87995d9 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Sun, 7 Dec 2025 15:36:18 -0800 Subject: [PATCH 13/14] fixing issue with test not returning --- cmd/card/imageviewer_test.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/cmd/card/imageviewer_test.go b/cmd/card/imageviewer_test.go index cc0361d..4d86e98 100644 --- a/cmd/card/imageviewer_test.go +++ b/cmd/card/imageviewer_test.go @@ -4,6 +4,7 @@ import ( "strings" "testing" + spinnerpkg "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" ) @@ -155,14 +156,15 @@ func TestImageModel_Update_ImageReady(t *testing.T) { func TestImageModel_Update_SpinnerTick(t *testing.T) { model := ImageRenderer("Mewtwo", "http://example.com/mewtwo.png") - // Create a spinner tick message msg := model.Spinner.Tick() - // Update should handle spinner ticks - newModel, cmd := model.Update(msg) + if _, ok := msg.(spinnerpkg.TickMsg); !ok { + t.Fatalf("expected spinner.TickMsg, got %T", msg) + } - // Should return a spinner command - if cmd == nil { + newModel, returnedCmd := model.Update(msg) + + if returnedCmd == nil { t.Error("Update with spinner.TickMsg should return a command") } From ead7b3f1dfb555178f85b79025b38b2e2a5a6bb5 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Sun, 7 Dec 2025 15:37:43 -0800 Subject: [PATCH 14/14] applying go fmt changes --- styling/styling.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/styling/styling.go b/styling/styling.go index d4a65a1..3e4ac93 100644 --- a/styling/styling.go +++ b/styling/styling.go @@ -15,32 +15,32 @@ var ( Gray = lipgloss.Color("#777777") Yellow = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#E1AD01", Dark: "#FFDE00"}) ColoredBullet = lipgloss.NewStyle(). - SetString("•"). - Foreground(lipgloss.Color("#FFCC00")) + SetString("•"). + Foreground(lipgloss.Color("#FFCC00")) CheckboxStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFCC00")) KeyMenu = lipgloss.NewStyle().Foreground(lipgloss.Color("#777777")) DocsLink = lipgloss.NewStyle(). - Foreground(lipgloss.AdaptiveColor{Light: "#E1AD01", Dark: "#FFCC00"}). - Render("\x1b]8;;https://docs.poke-cli.com\x1b\\docs.poke-cli.com\x1b]8;;\x1b\\") + Foreground(lipgloss.AdaptiveColor{Light: "#E1AD01", Dark: "#FFCC00"}). + Render("\x1b]8;;https://docs.poke-cli.com\x1b\\docs.poke-cli.com\x1b]8;;\x1b\\") StyleBold = lipgloss.NewStyle().Bold(true) StyleItalic = lipgloss.NewStyle().Italic(true) StyleUnderline = lipgloss.NewStyle().Underline(true) HelpBorder = lipgloss.NewStyle(). - BorderStyle(lipgloss.RoundedBorder()). - BorderForeground(lipgloss.Color("#FFCC00")) + BorderStyle(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("#FFCC00")) ErrorColor = lipgloss.NewStyle().Foreground(lipgloss.Color("#F2055C")) ErrorBorder = lipgloss.NewStyle(). - BorderStyle(lipgloss.RoundedBorder()). - BorderForeground(lipgloss.Color("#F2055C")) + BorderStyle(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("#F2055C")) WarningColor = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF8C00")) WarningBorder = lipgloss.NewStyle(). - BorderStyle(lipgloss.RoundedBorder()). - BorderForeground(lipgloss.Color("#FF8C00")) + BorderStyle(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("#FF8C00")) TypesTableBorder = lipgloss.NewStyle(). - BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color("#FFCC00")) + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("#FFCC00")) ColorMap = map[string]string{ "normal": "#B7B7A9", "fire": "#FF4422",