diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 924f972..41a9742 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -31,7 +31,7 @@ on:
- main
env:
- VERSION_NUMBER: 'v1.9.3'
+ VERSION_NUMBER: 'v1.9.4'
DOCKERHUB_REGISTRY_NAME: 'digitalghostdev/poke-cli'
AWS_REGION: 'us-west-2'
diff --git a/.goreleaser.yml b/.goreleaser.yml
index 475906b..9228c2d 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -14,7 +14,7 @@ builds:
- windows
- darwin
ldflags:
- - -s -w -X main.version=v1.9.3
+ - -s -w -X main.version=v1.9.4
archives:
- formats: [ 'zip' ]
diff --git a/Dockerfile b/Dockerfile
index 7f43ac5..1ddfbd5 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -8,7 +8,7 @@ RUN go mod download
COPY . .
-RUN go build -ldflags "-X main.version=v1.9.3" -o poke-cli .
+RUN go build -ldflags "-X main.version=v1.9.4" -o poke-cli .
# build 2
FROM --platform=$BUILDPLATFORM alpine:3.23
diff --git a/README.md b/README.md
index 25f767e..c3fa3ea 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
-
+
@@ -99,11 +99,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.9.3 [subcommand] [flag]
+ docker run --rm -it digitalghostdev/poke-cli:v1.9.4 [subcommand] [flag]
```
* Enter the container and use its shell:
```bash
- docker run --rm -it --name poke-cli --entrypoint /bin/sh digitalghostdev/poke-cli:v1.9.3 -c "cd /app && exec sh"
+ docker run --rm -it --name poke-cli --entrypoint /bin/sh digitalghostdev/poke-cli:v1.9.4 -c "cd /app && exec sh"
# placed into the /app directory, run the program with './poke-cli'
# example: ./poke-cli ability swift-swim
```
@@ -112,13 +112,13 @@ Cloudsmith is a fully cloud-based service that lets you easily create, store, an
> The `card` command renders TCG card images using your terminal's graphics protocol. When running inside Docker, pass your terminal's environment variables so image rendering works correctly:
> ```bash
> # Kitty
-> docker run --rm -it -e TERM -e KITTY_WINDOW_ID digitalghostdev/poke-cli:v1.9.3 card
+> docker run --rm -it -e TERM -e KITTY_WINDOW_ID digitalghostdev/poke-cli:v1.9.4 card
>
> # WezTerm, iTerm2, Ghostty, Konsole, Rio, Tabby
-> docker run --rm -it -e TERM -e TERM_PROGRAM digitalghostdev/poke-cli:v1.9.3 card
+> docker run --rm -it -e TERM -e TERM_PROGRAM digitalghostdev/poke-cli:v1.9.4 card
>
> # Windows Terminal (Sixel)
-> docker run --rm -it -e WT_SESSION digitalghostdev/poke-cli:v1.9.3 card
+> docker run --rm -it -e WT_SESSION digitalghostdev/poke-cli:v1.9.4 card
> ```
> If your terminal is not listed above, image rendering is not supported inside Docker.
diff --git a/card_data/pipelines/defs/extract/tcgcsv/extract_pricing.py b/card_data/pipelines/defs/extract/tcgcsv/extract_pricing.py
index 5c9babe..a093fd1 100644
--- a/card_data/pipelines/defs/extract/tcgcsv/extract_pricing.py
+++ b/card_data/pipelines/defs/extract/tcgcsv/extract_pricing.py
@@ -137,14 +137,15 @@ def pull_product_information(set_number: str) -> pl.DataFrame:
print(colored(" →", "blue"), f"Processing set: {set_number}")
product_id = SET_PRODUCT_MATCHING[set_number]
+ headers = {"User-Agent": "poke-cli/1.9"}
# Fetch product data
products_url = f"https://tcgcsv.com/tcgplayer/3/{product_id}/products"
- products_data = requests.get(products_url, timeout=30).json()
+ products_data = requests.get(products_url, timeout=30, headers=headers).json()
# Fetch pricing data
prices_url = f"https://tcgcsv.com/tcgplayer/3/{product_id}/prices"
- prices_data = requests.get(prices_url, timeout=30).json()
+ prices_data = requests.get(prices_url, timeout=30, headers=headers).json()
price_dict = {
price["productId"]: price.get("marketPrice")
diff --git a/card_data/pipelines/poke_cli_dbt/dbt_project.yml b/card_data/pipelines/poke_cli_dbt/dbt_project.yml
index b87090d..dc9e11d 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.9.3'
+version: '1.9.4'
profile: 'poke_cli_dbt'
diff --git a/cmd/item/item.go b/cmd/item/item.go
index e2c0a2c..fd82ecf 100644
--- a/cmd/item/item.go
+++ b/cmd/item/item.go
@@ -9,6 +9,7 @@ import (
"charm.land/lipgloss/v2"
"github.com/digitalghost-dev/poke-cli/cmd/utils"
"github.com/digitalghost-dev/poke-cli/connections"
+ "github.com/digitalghost-dev/poke-cli/constants"
"github.com/digitalghost-dev/poke-cli/structs"
"github.com/digitalghost-dev/poke-cli/styling"
)
@@ -78,7 +79,7 @@ func itemInfoContainer(output *strings.Builder, itemStruct structs.ItemJSONStruc
fullDoc = lipgloss.JoinVertical(lipgloss.Top, capitalizedItem, itemCost, itemCategory, "---", "Description:", missingData)
} else {
for _, entry := range itemStruct.FlavorTextEntries {
- if entry.Language.Name == "en" && entry.VersionGroup.Name == "sword-shield" {
+ if entry.Language.Name == "en" && entry.VersionGroup.Name == constants.VersionSwordShield {
if entry.Text != "" {
flavorTextEntry = entry.Text
fullDoc = lipgloss.JoinVertical(lipgloss.Top, capitalizedItem, itemCost, itemCategory, "---", "Description:", flavorTextEntry)
diff --git a/cmd/move/move.go b/cmd/move/move.go
index 1a3ae4c..00c3573 100644
--- a/cmd/move/move.go
+++ b/cmd/move/move.go
@@ -10,6 +10,7 @@ import (
"charm.land/lipgloss/v2"
"github.com/digitalghost-dev/poke-cli/cmd/utils"
"github.com/digitalghost-dev/poke-cli/connections"
+ "github.com/digitalghost-dev/poke-cli/constants"
"github.com/digitalghost-dev/poke-cli/structs"
"github.com/digitalghost-dev/poke-cli/styling"
"golang.org/x/text/cases"
@@ -110,12 +111,12 @@ func moveEffectContainer(output *strings.Builder, moveStruct structs.MoveJSONStr
continue
}
- if entry.VersionGroup.Name == "scarlet-violet" {
+ if entry.VersionGroup.Name == constants.VersionScarletViolet {
sv = entry.FlavorText
break
}
- if entry.VersionGroup.Name == "sword-shield" {
+ if entry.VersionGroup.Name == constants.VersionSwordShield {
swsh = entry.FlavorText
}
}
diff --git a/cmd/pokemon/pokemon.go b/cmd/pokemon/pokemon.go
index 51087d2..1e7da27 100644
--- a/cmd/pokemon/pokemon.go
+++ b/cmd/pokemon/pokemon.go
@@ -6,18 +6,13 @@ import (
"flag"
"fmt"
"io"
- "math"
"os"
- "sort"
"strings"
- "charm.land/lipgloss/v2"
"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"
)
// PokemonCommand processes the Pokémon command
@@ -83,150 +78,6 @@ func PokemonCommand() (string, error) {
capitalizedString := styling.CapitalizeResourceName(pokemonName)
- entry := func(w io.Writer) {
- for _, entry := range pokemonSpeciesStruct.FlavorTextEntries {
- if entry.Language.Name == "en" && (entry.Version.Name == "x" || entry.Version.Name == "shield" || entry.Version.Name == "scarlet") {
- flavorText := strings.ReplaceAll(entry.FlavorText, "\n", " ")
- flavorText = strings.Join(strings.Fields(flavorText), " ")
-
- wrapped := utils.WrapText(flavorText, 60)
- fmt.Fprintln(w, wrapped)
- return
- }
- }
- }
-
- eggInformation := func(w io.Writer) {
- var eggInformationSlice []string
-
- modernEggInformationNames := map[string]string{
- "indeterminate": "Amorphous",
- "ground": "Field",
- "humanshape": "Human-Like",
- "plant": "Grass",
- "no-eggs": "Undiscovered",
- }
-
- for _, entry := range pokemonSpeciesStruct.EggGroups {
- if name, exists := modernEggInformationNames[entry.Name]; exists {
- eggInformationSlice = append(eggInformationSlice, name)
- } else {
- eggInformationSlice = append(eggInformationSlice, cases.Title(language.English).String(entry.Name))
- }
- }
-
- sort.Strings(eggInformationSlice)
-
- genderRate := pokemonSpeciesStruct.GenderRate
- m := map[int]string{
- -1: "Genderless",
- 0: "0% F",
- 1: "12.5% F",
- 2: "25% F",
- 3: "37.5% F",
- 4: "50% F",
- 5: "62.5% F",
- 6: "75% F",
- 7: "87.5% F",
- 8: "100% F",
- }
-
- hatchCounter := pokemonSpeciesStruct.HatchCounter
-
- fmt.Fprintf(w,
- "\n%s %s %s\n%s %s %s\n%s %s %d",
- styling.ColoredBullet,
- "Egg Group(s):", strings.Join(eggInformationSlice, ", "),
- styling.ColoredBullet,
- "Gender Rate:", m[genderRate],
- styling.ColoredBullet,
- "Egg Cycles:", hatchCounter,
- )
- }
-
- effortValues := func(w io.Writer) {
- nameMapping := map[string]string{
- "hp": "HP",
- "attack": "Atk",
- "defense": "Def",
- "special-attack": "SpA",
- "special-defense": "SpD",
- "speed": "Spd",
- }
-
- var evs []string
-
- for _, effortValue := range pokemonStruct.Stats {
- if effortValue.Effort > 0 {
- name, ok := nameMapping[effortValue.Stat.Name]
- if !ok {
- name = "Missing from API"
- }
- evs = append(evs, fmt.Sprintf("%d %s", effortValue.Effort, name))
- }
- }
-
- fmt.Fprintf(w, "\n%s Effort Values: %s", styling.ColoredBullet, strings.Join(evs, ", "))
- }
-
- typing := func(w io.Writer) {
- var typeBoxes []string
-
- for _, pokeType := range pokemonStruct.Types {
- colorHex, exists := styling.ColorMap[pokeType.Type.Name]
- if exists {
- color := lipgloss.Color(colorHex)
- typeColorStyle := lipgloss.NewStyle().
- Align(lipgloss.Center).
- Foreground(lipgloss.Color("#FAFAFA")).
- Background(color).
- Margin(1, 1, 0, 0).
- Height(1).
- Width(14)
-
- rendered := typeColorStyle.Render(cases.Title(language.English).String(pokeType.Type.Name))
- typeBoxes = append(typeBoxes, rendered)
- }
- }
-
- joinedTypes := lipgloss.JoinHorizontal(lipgloss.Top, typeBoxes...)
- fmt.Fprintln(w, joinedTypes)
- }
-
- metrics := func(w io.Writer) {
- // Weight calculation
- weightKilograms := float64(pokemonStruct.Weight) / 10
- weightPounds := float64(weightKilograms) * 2.20462
-
- // Height calculation
- heightMeters := float64(pokemonStruct.Height) / 10
- heightFeet := heightMeters * 3.28084
- feet := int(heightFeet)
- inches := int(math.Round((heightFeet - float64(feet)) * 12)) // Use math.Round to avoid truncation
-
- // Adjust for rounding to 12 inches (carry over to the next foot)
- if inches == 12 {
- feet++
- inches = 0
- }
-
- fmt.Fprintf(w, "\n%s National Pokédex #: %d\n%s Weight: %.1fkg (%.1f lbs)\n%s Height: %.1fm (%d′%02d″)\n",
- styling.ColoredBullet, pokemonStruct.ID,
- styling.ColoredBullet, weightKilograms, weightPounds,
- styling.ColoredBullet, heightMeters, feet, inches)
- }
-
- species := func(w io.Writer) {
- if pokemonSpeciesStruct.EvolvesFromSpecies.Name != "" {
- evolvesFrom := pokemonSpeciesStruct.EvolvesFromSpecies.Name
-
- capitalizedPokemonName := styling.CapitalizeResourceName(evolvesFrom)
- fmt.Fprintf(w, "%s %s %s", styling.ColoredBullet, "Evolves from:", capitalizedPokemonName)
- } else {
- fmt.Fprintf(w, "%s %s", styling.ColoredBullet, "Basic Pokémon")
- }
- }
-
var (
entryOutput bytes.Buffer
eggGroupOutput bytes.Buffer
@@ -236,12 +87,12 @@ func PokemonCommand() (string, error) {
effortValuesOutput bytes.Buffer
)
- entry(&entryOutput)
- eggInformation(&eggGroupOutput)
- typing(&typeOutput)
- metrics(&metricsOutput)
- species(&speciesOutput)
- effortValues(&effortValuesOutput)
+ renderEntry(&entryOutput, pokemonSpeciesStruct)
+ renderEggInformation(&eggGroupOutput, pokemonSpeciesStruct)
+ renderTyping(&typeOutput, pokemonStruct)
+ renderMetrics(&metricsOutput, pokemonStruct)
+ renderSpecies(&speciesOutput, pokemonSpeciesStruct)
+ renderEffortValues(&effortValuesOutput, pokemonStruct)
fmt.Fprintf(&output,
"Your selected Pokémon: %s\n%s\n%s%s%s%s%s\n",
diff --git a/cmd/pokemon/render.go b/cmd/pokemon/render.go
new file mode 100644
index 0000000..00e22ac
--- /dev/null
+++ b/cmd/pokemon/render.go
@@ -0,0 +1,148 @@
+package pokemon
+
+import (
+ "fmt"
+ "io"
+ "math"
+ "sort"
+ "strings"
+
+ "charm.land/lipgloss/v2"
+ "github.com/digitalghost-dev/poke-cli/cmd/utils"
+ "github.com/digitalghost-dev/poke-cli/structs"
+ "github.com/digitalghost-dev/poke-cli/styling"
+ "golang.org/x/text/cases"
+ "golang.org/x/text/language"
+)
+
+func renderEggInformation(w io.Writer, s structs.PokemonSpeciesJSONStruct) {
+ var eggInformationSlice []string
+
+ modernEggInformationNames := map[string]string{
+ "indeterminate": "Amorphous",
+ "ground": "Field",
+ "humanshape": "Human-Like",
+ "plant": "Grass",
+ "no-eggs": "Undiscovered",
+ }
+
+ for _, entry := range s.EggGroups {
+ if name, exists := modernEggInformationNames[entry.Name]; exists {
+ eggInformationSlice = append(eggInformationSlice, name)
+ } else {
+ eggInformationSlice = append(eggInformationSlice, cases.Title(language.English).String(entry.Name))
+ }
+ }
+
+ sort.Strings(eggInformationSlice)
+
+ m := map[int]string{
+ -1: "Genderless",
+ 0: "0% F",
+ 1: "12.5% F",
+ 2: "25% F",
+ 3: "37.5% F",
+ 4: "50% F",
+ 5: "62.5% F",
+ 6: "75% F",
+ 7: "87.5% F",
+ 8: "100% F",
+ }
+
+ fmt.Fprintf(w,
+ "\n%s %s %s\n%s %s %s\n%s %s %d",
+ styling.ColoredBullet,
+ "Egg Group(s):", strings.Join(eggInformationSlice, ", "),
+ styling.ColoredBullet,
+ "Gender Rate:", m[s.GenderRate],
+ styling.ColoredBullet,
+ "Egg Cycles:", s.HatchCounter,
+ )
+}
+
+func renderEffortValues(w io.Writer, s structs.PokemonJSONStruct) {
+ nameMapping := map[string]string{
+ "hp": "HP",
+ "attack": "Atk",
+ "defense": "Def",
+ "special-attack": "SpA",
+ "special-defense": "SpD",
+ "speed": "Spd",
+ }
+
+ var evs []string
+
+ for _, effortValue := range s.Stats {
+ if effortValue.Effort > 0 {
+ name, ok := nameMapping[effortValue.Stat.Name]
+ if !ok {
+ name = "Missing from API"
+ }
+ evs = append(evs, fmt.Sprintf("%d %s", effortValue.Effort, name))
+ }
+ }
+
+ fmt.Fprintf(w, "\n%s Effort Values: %s", styling.ColoredBullet, strings.Join(evs, ", "))
+}
+
+func renderEntry(w io.Writer, s structs.PokemonSpeciesJSONStruct) {
+ for _, entry := range s.FlavorTextEntries {
+ if entry.Language.Name == "en" && (entry.Version.Name == "x" || entry.Version.Name == "shield" || entry.Version.Name == "scarlet") {
+ flavorText := strings.ReplaceAll(entry.FlavorText, "\n", " ")
+ flavorText = strings.Join(strings.Fields(flavorText), " ")
+ fmt.Fprintln(w, utils.WrapText(flavorText, 60))
+ return
+ }
+ }
+}
+
+func renderMetrics(w io.Writer, s structs.PokemonJSONStruct) {
+ weightKilograms := float64(s.Weight) / 10
+ weightPounds := float64(weightKilograms) * 2.20462
+
+ heightMeters := float64(s.Height) / 10
+ heightFeet := heightMeters * 3.28084
+ feet := int(heightFeet)
+ inches := int(math.Round((heightFeet - float64(feet)) * 12))
+
+ if inches == 12 {
+ feet++
+ inches = 0
+ }
+
+ fmt.Fprintf(w, "\n%s National Pokédex #: %d\n%s Weight: %.1fkg (%.1f lbs)\n%s Height: %.1fm (%d′%02d″)\n",
+ styling.ColoredBullet, s.ID,
+ styling.ColoredBullet, weightKilograms, weightPounds,
+ styling.ColoredBullet, heightMeters, feet, inches)
+}
+
+func renderSpecies(w io.Writer, s structs.PokemonSpeciesJSONStruct) {
+ if s.EvolvesFromSpecies.Name != "" {
+ capitalizedPokemonName := styling.CapitalizeResourceName(s.EvolvesFromSpecies.Name)
+ fmt.Fprintf(w, "%s %s %s", styling.ColoredBullet, "Evolves from:", capitalizedPokemonName)
+ } else {
+ fmt.Fprintf(w, "%s %s", styling.ColoredBullet, "Basic Pokémon")
+ }
+}
+
+func renderTyping(w io.Writer, s structs.PokemonJSONStruct) {
+ var typeBoxes []string
+
+ for _, pokeType := range s.Types {
+ colorHex, exists := styling.ColorMap[pokeType.Type.Name]
+ if exists {
+ typeColorStyle := lipgloss.NewStyle().
+ Align(lipgloss.Center).
+ Foreground(lipgloss.Color("#FAFAFA")).
+ Background(lipgloss.Color(colorHex)).
+ Margin(1, 1, 0, 0).
+ Height(1).
+ Width(14)
+
+ rendered := typeColorStyle.Render(cases.Title(language.English).String(pokeType.Type.Name))
+ typeBoxes = append(typeBoxes, rendered)
+ }
+ }
+
+ fmt.Fprintln(w, lipgloss.JoinHorizontal(lipgloss.Top, typeBoxes...))
+}
diff --git a/cmd/pokemon/render_test.go b/cmd/pokemon/render_test.go
new file mode 100644
index 0000000..1d4c338
--- /dev/null
+++ b/cmd/pokemon/render_test.go
@@ -0,0 +1,343 @@
+package pokemon
+
+import (
+ "bytes"
+ "strings"
+ "testing"
+
+ "github.com/digitalghost-dev/poke-cli/structs"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestRenderEntry(t *testing.T) {
+ tests := []struct {
+ name string
+ species structs.PokemonSpeciesJSONStruct
+ contains string
+ }{
+ {
+ name: "first matching english entry returned",
+ species: structs.PokemonSpeciesJSONStruct{
+ FlavorTextEntries: []struct {
+ FlavorText string `json:"flavor_text"`
+ Language struct {
+ Name string `json:"name"`
+ URL string `json:"url"`
+ } `json:"language"`
+ Version struct {
+ Name string `json:"name"`
+ URL string `json:"url"`
+ } `json:"version"`
+ }{
+ {FlavorText: "A scarlet entry.", Language: struct {
+ Name string `json:"name"`
+ URL string `json:"url"`
+ }{Name: "en"}, Version: struct {
+ Name string `json:"name"`
+ URL string `json:"url"`
+ }{Name: "scarlet"}},
+ {FlavorText: "A shield entry.", Language: struct {
+ Name string `json:"name"`
+ URL string `json:"url"`
+ }{Name: "en"}, Version: struct {
+ Name string `json:"name"`
+ URL string `json:"url"`
+ }{Name: "shield"}},
+ },
+ },
+ contains: "A scarlet entry.",
+ },
+ {
+ name: "non-english entries are skipped",
+ species: structs.PokemonSpeciesJSONStruct{
+ FlavorTextEntries: []struct {
+ FlavorText string `json:"flavor_text"`
+ Language struct {
+ Name string `json:"name"`
+ URL string `json:"url"`
+ } `json:"language"`
+ Version struct {
+ Name string `json:"name"`
+ URL string `json:"url"`
+ } `json:"version"`
+ }{
+ {FlavorText: "Un texto en español.", Language: struct {
+ Name string `json:"name"`
+ URL string `json:"url"`
+ }{Name: "es"}, Version: struct {
+ Name string `json:"name"`
+ URL string `json:"url"`
+ }{Name: "scarlet"}},
+ },
+ },
+ contains: "",
+ },
+ {
+ name: "empty flavor text entries",
+ species: structs.PokemonSpeciesJSONStruct{},
+ contains: "",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ var buf bytes.Buffer
+ renderEntry(&buf, tt.species)
+ if tt.contains == "" {
+ assert.Empty(t, buf.String())
+ } else {
+ assert.Contains(t, buf.String(), tt.contains)
+ }
+ })
+ }
+}
+
+func TestRenderEggInformation(t *testing.T) {
+ tests := []struct {
+ name string
+ species structs.PokemonSpeciesJSONStruct
+ contains []string
+ }{
+ {
+ name: "known legacy egg group names are modernized",
+ species: structs.PokemonSpeciesJSONStruct{
+ EggGroups: []struct {
+ Name string `json:"name"`
+ URL string `json:"url"`
+ }{{Name: "indeterminate"}, {Name: "ground"}},
+ GenderRate: 4,
+ HatchCounter: 20,
+ },
+ contains: []string{"Amorphous", "Field", "50% F", "20"},
+ },
+ {
+ name: "genderless pokemon",
+ species: structs.PokemonSpeciesJSONStruct{
+ EggGroups: []struct {
+ Name string `json:"name"`
+ URL string `json:"url"`
+ }{{Name: "no-eggs"}},
+ GenderRate: -1,
+ HatchCounter: 120,
+ },
+ contains: []string{"Undiscovered", "Genderless", "120"},
+ },
+ {
+ name: "regular egg group title-cased",
+ species: structs.PokemonSpeciesJSONStruct{
+ EggGroups: []struct {
+ Name string `json:"name"`
+ URL string `json:"url"`
+ }{{Name: "monster"}},
+ GenderRate: 1,
+ HatchCounter: 5,
+ },
+ contains: []string{"Monster", "12.5% F"},
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ var buf bytes.Buffer
+ renderEggInformation(&buf, tt.species)
+ output := buf.String()
+ for _, s := range tt.contains {
+ assert.Contains(t, output, s)
+ }
+ })
+ }
+}
+
+func TestRenderEffortValues(t *testing.T) {
+ tests := []struct {
+ name string
+ pokemon structs.PokemonJSONStruct
+ contains string
+ absent string
+ }{
+ {
+ name: "single EV stat",
+ pokemon: structs.PokemonJSONStruct{
+ Stats: []struct {
+ BaseStat int `json:"base_stat"`
+ Effort int `json:"effort"`
+ Stat struct {
+ Name string `json:"name"`
+ } `json:"stat"`
+ }{
+ {Effort: 2, Stat: struct{ Name string `json:"name"` }{Name: "speed"}},
+ {Effort: 0, Stat: struct{ Name string `json:"name"` }{Name: "attack"}},
+ },
+ },
+ contains: "2 Spd",
+ absent: "Atk",
+ },
+ {
+ name: "unknown stat name falls back",
+ pokemon: structs.PokemonJSONStruct{
+ Stats: []struct {
+ BaseStat int `json:"base_stat"`
+ Effort int `json:"effort"`
+ Stat struct {
+ Name string `json:"name"`
+ } `json:"stat"`
+ }{
+ {Effort: 1, Stat: struct{ Name string `json:"name"` }{Name: "mystery-stat"}},
+ },
+ },
+ contains: "Missing from API",
+ },
+ {
+ name: "no EVs produces empty list",
+ pokemon: structs.PokemonJSONStruct{
+ Stats: []struct {
+ BaseStat int `json:"base_stat"`
+ Effort int `json:"effort"`
+ Stat struct {
+ Name string `json:"name"`
+ } `json:"stat"`
+ }{
+ {Effort: 0, Stat: struct{ Name string `json:"name"` }{Name: "hp"}},
+ },
+ },
+ contains: "Effort Values:",
+ absent: "HP",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ var buf bytes.Buffer
+ renderEffortValues(&buf, tt.pokemon)
+ output := buf.String()
+ assert.Contains(t, output, tt.contains)
+ if tt.absent != "" {
+ assert.NotContains(t, output, tt.absent)
+ }
+ })
+ }
+}
+
+func TestRenderMetrics(t *testing.T) {
+ tests := []struct {
+ name string
+ pokemon structs.PokemonJSONStruct
+ contains []string
+ }{
+ {
+ name: "pikachu-like metrics",
+ pokemon: structs.PokemonJSONStruct{ID: 25, Weight: 60, Height: 4},
+ contains: []string{
+ "National Pokédex #: 25",
+ "6.0kg",
+ "0.4m",
+ },
+ },
+ {
+ name: "heavy pokemon",
+ pokemon: structs.PokemonJSONStruct{ID: 131, Weight: 2160, Height: 20},
+ contains: []string{
+ "National Pokédex #: 131",
+ "216.0kg",
+ "2.0m",
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ var buf bytes.Buffer
+ renderMetrics(&buf, tt.pokemon)
+ output := buf.String()
+ for _, s := range tt.contains {
+ assert.Contains(t, output, s)
+ }
+ })
+ }
+}
+
+func TestRenderSpecies(t *testing.T) {
+ tests := []struct {
+ name string
+ species structs.PokemonSpeciesJSONStruct
+ contains string
+ }{
+ {
+ name: "evolves from a previous stage",
+ species: structs.PokemonSpeciesJSONStruct{
+ EvolvesFromSpecies: struct {
+ Name string `json:"name"`
+ URL string `json:"url"`
+ }{Name: "pikachu"},
+ },
+ contains: "Evolves from: Pikachu",
+ },
+ {
+ name: "basic pokemon with no pre-evolution",
+ species: structs.PokemonSpeciesJSONStruct{},
+ contains: "Basic Pokémon",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ var buf bytes.Buffer
+ renderSpecies(&buf, tt.species)
+ assert.Contains(t, buf.String(), tt.contains)
+ })
+ }
+}
+
+func TestRenderTyping(t *testing.T) {
+ tests := []struct {
+ name string
+ pokemon structs.PokemonJSONStruct
+ contains string
+ }{
+ {
+ name: "fire type renders type name",
+ pokemon: structs.PokemonJSONStruct{
+ Types: []struct {
+ Slot int `json:"slot"`
+ Type struct {
+ Name string `json:"name"`
+ URL string `json:"url"`
+ } `json:"type"`
+ }{{Type: struct {
+ Name string `json:"name"`
+ URL string `json:"url"`
+ }{Name: "fire"}}},
+ },
+ contains: "Fire",
+ },
+ {
+ name: "unknown type is skipped",
+ pokemon: structs.PokemonJSONStruct{
+ Types: []struct {
+ Slot int `json:"slot"`
+ Type struct {
+ Name string `json:"name"`
+ URL string `json:"url"`
+ } `json:"type"`
+ }{{Type: struct {
+ Name string `json:"name"`
+ URL string `json:"url"`
+ }{Name: "faketype"}}},
+ },
+ contains: "",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ var buf bytes.Buffer
+ renderTyping(&buf, tt.pokemon)
+ output := buf.String()
+ if tt.contains == "" {
+ assert.Empty(t, strings.TrimSpace(output))
+ } else {
+ assert.Contains(t, output, tt.contains)
+ }
+ })
+ }
+}
diff --git a/constants/values.go b/constants/values.go
new file mode 100644
index 0000000..f918223
--- /dev/null
+++ b/constants/values.go
@@ -0,0 +1,8 @@
+package constants
+
+const (
+ VersionScarletViolet = "scarlet-violet"
+ VersionSwordShield = "sword-shield"
+ VersionSunMoon = "sun-moon"
+ VersionXY = "x-y"
+)
diff --git a/flags/pokemonflagset.go b/flags/pokemonflagset.go
index 5c6b567..27cfda0 100644
--- a/flags/pokemonflagset.go
+++ b/flags/pokemonflagset.go
@@ -19,6 +19,7 @@ import (
"charm.land/lipgloss/v2/table"
cmdutils "github.com/digitalghost-dev/poke-cli/cmd/utils"
"github.com/digitalghost-dev/poke-cli/connections"
+ "github.com/digitalghost-dev/poke-cli/constants"
"github.com/digitalghost-dev/poke-cli/structs"
"github.com/digitalghost-dev/poke-cli/styling"
"github.com/disintegration/imaging"
@@ -427,7 +428,7 @@ func MovesFlag(w io.Writer, endpoint string, pokemonName string) error {
eligibleMoves := 0
for _, pokeMove := range pokemonStruct.Moves {
for _, detail := range pokeMove.VersionGroupDetails {
- if detail.VersionGroup.Name != "scarlet-violet" || detail.MoveLearnedMethod.Name != "level-up" {
+ if detail.VersionGroup.Name != constants.VersionScarletViolet || detail.MoveLearnedMethod.Name != "level-up" {
continue
}
diff --git a/nfpm.yaml b/nfpm.yaml
index c3b5afa..86c4449 100644
--- a/nfpm.yaml
+++ b/nfpm.yaml
@@ -1,7 +1,7 @@
name: "poke-cli"
arch: "arm64"
platform: "linux"
-version: "v1.9.3"
+version: "v1.9.4"
section: "default"
version_schema: semver
maintainer: "Christian S"
diff --git a/structs/structs_test.go b/structs/structs_test.go
index 3a70b83..f50ee52 100644
--- a/structs/structs_test.go
+++ b/structs/structs_test.go
@@ -3,8 +3,19 @@ package structs
import (
"encoding/json"
"testing"
+
+ "github.com/stretchr/testify/assert"
)
+func TestGetResourceName(t *testing.T) {
+ assert.Equal(t, "strong-jaw", AbilityJSONStruct{Name: "strong-jaw"}.GetResourceName())
+ assert.Equal(t, "poke-ball", ItemJSONStruct{Name: "poke-ball"}.GetResourceName())
+ assert.Equal(t, "thunderbolt", MoveJSONStruct{Name: "thunderbolt"}.GetResourceName())
+ assert.Equal(t, "pikachu", PokemonJSONStruct{Name: "pikachu"}.GetResourceName())
+ assert.Equal(t, "pikachu", PokemonSpeciesJSONStruct{Name: "pikachu"}.GetResourceName())
+ assert.Equal(t, "fire", TypesJSONStruct{Name: "fire"}.GetResourceName())
+}
+
func TestPokemonJSONStruct_Unmarshal(t *testing.T) {
// Sample JSON data for a Pokémon
jsonData := `{
diff --git a/testdata/main_latest_flag.golden b/testdata/main_latest_flag.golden
index 5d9163c..2848c86 100644
--- a/testdata/main_latest_flag.golden
+++ b/testdata/main_latest_flag.golden
@@ -2,6 +2,6 @@
┃ ┃
┃ Latest available release ┃
┃ on GitHub: ┃
-┃ • v1.9.2 ┃
+┃ • v1.9.3 ┃
┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
diff --git a/web/pyproject.toml b/web/pyproject.toml
index 8f1d0a3..b717b61 100644
--- a/web/pyproject.toml
+++ b/web/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "web"
-version = "1.9.3"
+version = "1.9.4"
description = "Streamlit dashboard for browsing and visualizing Pokémon TCG tournament standings and results."
readme = "README.md"
requires-python = ">=3.12"