diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c86a98e..d24b4fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ on: - main env: - VERSION_NUMBER: 'v1.3.3' + VERSION_NUMBER: 'v1.4.0' DOCKERHUB_REGISTRY_NAME: 'digitalghostdev/poke-cli' AWS_REGION: 'us-west-2' diff --git a/.goreleaser.yaml b/.goreleaser.yml similarity index 88% rename from .goreleaser.yaml rename to .goreleaser.yml index 557e2c2..cf6446a 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yml @@ -14,7 +14,7 @@ builds: - windows - darwin ldflags: - - -s -w -X main.version=v1.3.3 + - -s -w -X main.version=v1.4.0 archives: - formats: [ 'zip' ] @@ -28,7 +28,7 @@ archives: # use zip for windows archives format_overrides: - goos: windows - - formats: [ 'zip' ] + formats: [ 'zip' ] changelog: sort: asc @@ -51,6 +51,6 @@ homebrew_casks: hooks: post: install: | - if system_command("/usr/bin/xattr", args: ["-h"]).exit_status == 0 + if OS.mac? && system_command("/usr/bin/xattr", args: ["-h"]).exit_status == 0 system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/poke-cli"] end \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index cc68419..58c0abc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # build 1 -FROM golang:1.24.4-alpine3.21 AS build +FROM golang:1.24.4-alpine3.22 AS build WORKDIR /app @@ -8,13 +8,14 @@ RUN go mod download COPY . . -RUN go build -ldflags "-X main.version=v1.3.3" -o poke-cli . +RUN go build -ldflags "-X main.version=v1.4.0" -o poke-cli . # build 2 -FROM --platform=$BUILDPLATFORM alpine:latest +FROM --platform=$BUILDPLATFORM alpine:3.22 # Install only necessary packages and remove them after use -RUN apk add --no-cache shadow && \ +RUN apk upgrade && \ + apk add --no-cache shadow && \ addgroup -S poke_group && adduser -S poke_user -G poke_group && \ sed -i 's/^root:.*/root:!*:0:0:root:\/root:\/sbin\/nologin/' /etc/passwd && \ apk del shadow diff --git a/README.md b/README.md index 7b1541d..7a8d474 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
@@ -77,22 +77,24 @@ View future plans in the [Roadmap](#roadmap) section. 3. Choose how to interact with the container: * Run a single command and exit: ```bash - docker run --rm -it digitalghostdev/poke-cli:v1.3.3 [subcommand] flag] + docker run --rm -it digitalghostdev/poke-cli:v1.4.0 [subcommand] flag] ``` * Enter the container and use its shell: ```bash - docker run --rm -it --name poke-cli --entrypoint /bin/sh digitalghostdev/poke-cli:v1.3.3 -c "cd /app && exec sh" + docker run --rm -it --name poke-cli --entrypoint /bin/sh digitalghostdev/poke-cli:v1.4.0 -c "cd /app && exec sh" # placed into the /app directory, run the program with './poke-cli' # example: ./poke-cli ability swift-swim ``` ### Homebrew -```bash -brew install --cask digitalghost-dev/tap/poke-cli - -# verify -poke-cli -v -``` +1. Install the Cask: + ```bash + brew install --cask digitalghost-dev/tap/poke-cli + ```` +2. Verify install! + ```bash + poke-cli -v + ``` ### Source @@ -121,6 +123,7 @@ By running `poke-cli [-h | --help]`, it'll display information on how to use the │ │ │ COMMANDS: │ │ ability Get details about an ability │ +│ item Get details about an item │ │ move Get details about a move │ │ natures Get details about all natures │ │ pokemon Get details about a Pokémon │ @@ -130,6 +133,9 @@ By running `poke-cli [-h | --help]`, it'll display information on how to use the │ hint: when calling a resource with a space, use a hyphen │ │ example: poke-cli ability strong-jaw │ │ example: poke-cli pokemon flutter-mane │ +│ │ +│ ↓ ctrl/cmd + click for docs/guides │ +│ docs.poke-cli.com │ ╰──────────────────────────────────────────────────────────╯ ``` @@ -138,20 +144,25 @@ By running `poke-cli [-h | --help]`, it'll display information on how to use the ## Roadmap Below is a list of the planned/completed commands and flags: -- [x] `ability`: get data about a specific ability. +- [x] `ability`: get data about an ability. - [x] `-p | --pokemon`: display Pokémon that learn this ability. -- [ ] `berry`: get data about a specific berry. -- [ ] `item`: get data about a specific item. -- [x] `move`: get data about a specific move. +- [ ] `berry`: get data about a berry. +- [x] `item`: get data about an item. +- [x] `move`: get data about a move. - [ ] `-p | --pokemon`: display Pokémon that learn this move. - [x] `natures`: get data about natures. -- [x] `pokemon`: get data about a specific Pokémon. +- [x] `pokemon`: get data about a Pokémon. - [x] `-a | --abilities`: display the Pokémon's abilities. - [x] `-i | --image`: display a pixel image of the Pokémon. - [x] `-s | --stats`: display the Pokémon's base stats. - [x] `-t | --types`: display the Pokémon's typing. - [x] `-m | --moves`: display learnable moves. -- [x] `search`: search for a resource (`ability`, `berry`, `pokemon`, `move`) +- [ ] `search`: search for a resource + - [x] `ability` + - [ ] `berry` + - [ ] `item` + - [x] `move` + - [x] `pokemon` - [ ] `speed`: compare speed stats between two Pokémon. - [x] `types`: get data about a specific typing. diff --git a/cli.go b/cli.go index 9a489c0..fc7b1a9 100644 --- a/cli.go +++ b/cli.go @@ -4,6 +4,7 @@ import ( "flag" "fmt" "github.com/digitalghost-dev/poke-cli/cmd/ability" + "github.com/digitalghost-dev/poke-cli/cmd/item" "github.com/digitalghost-dev/poke-cli/cmd/move" "github.com/digitalghost-dev/poke-cli/cmd/natures" "github.com/digitalghost-dev/poke-cli/cmd/pokemon" @@ -66,6 +67,7 @@ func runCLI(args []string) int { fmt.Sprintf("\n\t%-15s %s", "-v, --version", "Prints the current version"), "\n\n", styling.StyleBold.Render("COMMANDS:"), fmt.Sprintf("\n\t%-15s %s", "ability", "Get details about an ability"), + fmt.Sprintf("\n\t%-15s %s", "item", "Get details about an item"), fmt.Sprintf("\n\t%-15s %s", "move", "Get details about a move"), fmt.Sprintf("\n\t%-15s %s", "natures", "Get details about all natures"), fmt.Sprintf("\n\t%-15s %s", "pokemon", "Get details about a Pokémon"), @@ -99,6 +101,7 @@ func runCLI(args []string) int { commands := map[string]func() int{ "ability": utils.HandleCommandOutput(ability.AbilityCommand), + "item": utils.HandleCommandOutput(item.ItemCommand), "move": utils.HandleCommandOutput(move.MoveCommand), "natures": utils.HandleCommandOutput(natures.NaturesCommand), "pokemon": utils.HandleCommandOutput(pokemon.PokemonCommand), @@ -133,6 +136,7 @@ func runCLI(args []string) int { fmt.Sprintf("\n\t%-15s", fmt.Sprintf("'%s' is not a valid command.\n", cmdArg)), styling.StyleBold.Render("\nCommands:"), fmt.Sprintf("\n\t%-15s %s", "ability", "Get details about an ability"), + fmt.Sprintf("\n\t%-15s %s", "item", "Get details about an item"), fmt.Sprintf("\n\t%-15s %s", "move", "Get details about a move"), fmt.Sprintf("\n\t%-15s %s", "natures", "Get details about all natures"), fmt.Sprintf("\n\t%-15s %s", "pokemon", "Get details about a Pokémon"), diff --git a/cmd/item/item.go b/cmd/item/item.go new file mode 100644 index 0000000..ca2cb0a --- /dev/null +++ b/cmd/item/item.go @@ -0,0 +1,96 @@ +package item + +import ( + "flag" + "fmt" + "github.com/charmbracelet/lipgloss" + "github.com/digitalghost-dev/poke-cli/cmd/utils" + "github.com/digitalghost-dev/poke-cli/connections" + "github.com/digitalghost-dev/poke-cli/structs" + "github.com/digitalghost-dev/poke-cli/styling" + "golang.org/x/text/cases" + "golang.org/x/text/language" + "os" + "strings" +) + +func ItemCommand() (string, error) { + var output strings.Builder + + flag.Usage = func() { + helpMessage := styling.HelpBorder.Render( + "Get details about a specific item.\n\n", + styling.StyleBold.Render("USAGE:"), + fmt.Sprintf("\n\t%s %s %s %s", "poke-cli", styling.StyleBold.Render("item"), "", "[flag]"), + fmt.Sprintf("\n\t%-30s", styling.StyleItalic.Render("Use a hyphen when typing a name with a space.")), + "\n\n", + styling.StyleBold.Render("FLAGS:"), + fmt.Sprintf("\n\t%-30s %s", "-h, --help", "Prints the help menu."), + ) + output.WriteString(helpMessage) + } + + args := os.Args + + flag.Parse() + + if len(os.Args) == 3 && (os.Args[2] == "-h" || os.Args[2] == "--help") { + flag.Usage() + return output.String(), nil + } + + if err := utils.ValidateItemArgs(os.Args); err != nil { + output.WriteString(err.Error()) + return output.String(), err + } + + endpoint := strings.ToLower(args[1]) + itemName := strings.ToLower(args[2]) + + 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 + } + + itemInfoContainer(&output, itemStruct, itemName) + + return output.String(), nil +} + +func itemInfoContainer(output *strings.Builder, itemStruct structs.ItemJSONStruct, itemName string) { + capitalizedItem := styling.StyleBold.Render(cases.Title(language.English).String(strings.ReplaceAll(itemName, "-", " "))) + itemCost := fmt.Sprintf("Cost: %d", itemStruct.Cost) + itemCategory := "Category: " + cases.Title(language.English).String(strings.ReplaceAll(itemStruct.Category.Name, "-", " ")) + + docStyle := lipgloss.NewStyle(). + Padding(1, 2). + BorderStyle(lipgloss.ThickBorder()). + BorderForeground(lipgloss.AdaptiveColor{Light: "#444", Dark: "#EEE"}). + Width(32) + + var flavorTextEntry string + var missingData string + var fullDoc string + + if len(itemStruct.FlavorTextEntries) == 0 { + missingData = styling.StyleItalic.Render("Missing data from API") + 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.Text != "" { + flavorTextEntry = entry.Text + fullDoc = lipgloss.JoinVertical(lipgloss.Top, capitalizedItem, itemCost, itemCategory, "---", "Description:", flavorTextEntry) + break + } + } + } + } + + output.WriteString(docStyle.Render(fullDoc)) + output.WriteString("\n") +} diff --git a/cmd/item/item_test.go b/cmd/item/item_test.go new file mode 100644 index 0000000..dccc619 --- /dev/null +++ b/cmd/item/item_test.go @@ -0,0 +1,70 @@ +package item + +import ( + "github.com/digitalghost-dev/poke-cli/cmd/utils" + "github.com/digitalghost-dev/poke-cli/styling" + "github.com/stretchr/testify/assert" + "os" + "testing" +) + +func TestItemCommand(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 + expectedOutput string + expectedError bool + }{ + { + name: "Item help flag", + args: []string{"item", "--help"}, + expectedOutput: utils.LoadGolden(t, "item_help.golden"), + }, + { + name: "Item help flag", + args: []string{"item", "-h"}, + expectedOutput: utils.LoadGolden(t, "item_help.golden"), + }, + { + name: "Select 'choice-band' as item", + args: []string{"item", "choice-band"}, + expectedOutput: utils.LoadGolden(t, "item.golden"), + }, + { + name: "Select 'clear-amulet' as item with missing data", + args: []string{"item", "clear-amulet"}, + expectedOutput: utils.LoadGolden(t, "item_missing_data.golden"), + }, + { + name: "Too many arguments", + args: []string{"item", "dubious-disc", "--help"}, + expectedOutput: utils.LoadGolden(t, "item_too_many_args.golden"), + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + originalArgs := os.Args + os.Args = append([]string{"poke-cli"}, tt.args...) + defer func() { os.Args = originalArgs }() + + output, _ := ItemCommand() + cleanOutput := styling.StripANSI(output) + + assert.Equal(t, tt.expectedOutput, cleanOutput, "Output should match expected") + }) + } +} diff --git a/cmd/move/move_test.go b/cmd/move/move_test.go index 23459eb..837d798 100644 --- a/cmd/move/move_test.go +++ b/cmd/move/move_test.go @@ -38,7 +38,7 @@ func TestMoveCommand(t *testing.T) { expectedOutput: utils.LoadGolden(t, "move_help.golden"), }, { - name: "Select 'Shadow-Ball' as move", + name: "Select 'shadow-ball' as move", args: []string{"move", "shadow-ball"}, expectedOutput: utils.LoadGolden(t, "move.golden"), }, diff --git a/cmd/natures/natures.go b/cmd/natures/natures.go index 7ef7677..bdd16e5 100644 --- a/cmd/natures/natures.go +++ b/cmd/natures/natures.go @@ -14,7 +14,6 @@ import ( func NaturesCommand() (string, error) { var output strings.Builder - // Define the usage function flag.Usage = func() { helpMessage := styling.HelpBorder.Render( "Get details about all natures.\n\n", diff --git a/cmd/pokemon/pokemon.go b/cmd/pokemon/pokemon.go index b6bd1bd..b9eade9 100644 --- a/cmd/pokemon/pokemon.go +++ b/cmd/pokemon/pokemon.go @@ -104,8 +104,8 @@ func PokemonCommand() (string, error) { // Call the ImageFlag function with the specified size if err := flags.ImageFlag(&output, endpoint, pokemonName, size); err != nil { - output.WriteString(fmt.Sprintf("error parsing flags: %v\n", err)) - return "", fmt.Errorf("error parsing flags: %w", err) + output.WriteString(fmt.Sprintf("%v\n", err)) + return output.String(), fmt.Errorf("%w", err) } } diff --git a/cmd/pokemon/pokemon_test.go b/cmd/pokemon/pokemon_test.go index 5222430..5a3ceae 100644 --- a/cmd/pokemon/pokemon_test.go +++ b/cmd/pokemon/pokemon_test.go @@ -25,7 +25,7 @@ func TestPokemonCommand(t *testing.T) { name string args []string expectedOutput string - wantError bool + expectedError bool }{ { name: "Pokemon help flag", @@ -43,10 +43,22 @@ func TestPokemonCommand(t *testing.T) { expectedOutput: utils.LoadGolden(t, "pokemon_image.golden"), }, { - name: "Pokemon invalid image flag", + name: "Pokemon image flag missing size", args: []string{"pokemon", "tryanitar", "--image="}, - expectedOutput: utils.LoadGolden(t, "pokemon_invalid_image_flag.golden"), - wantError: true, + expectedOutput: utils.LoadGolden(t, "pokemon_image_flag_missing_size.golden"), + expectedError: true, + }, + { + name: "Pokemon image flag non-valid size", + args: []string{"pokemon", "floatzel", "--image=xl"}, + expectedOutput: utils.LoadGolden(t, "pokemon_image_flag_non-valid_size.golden"), + expectedError: true, + }, + { + name: "Pokemon image flag empty flag", + args: []string{"pokemon", "gastly", "--"}, + expectedOutput: utils.LoadGolden(t, "pokemon_image_flag_empty_flag.golden"), + expectedError: true, }, { name: "Pokemon stats flag", diff --git a/cmd/search/search.go b/cmd/search/search.go index 6849904..1f5efbc 100644 --- a/cmd/search/search.go +++ b/cmd/search/search.go @@ -10,9 +10,7 @@ import ( "os" ) -// SearchCommand is the main function for the "search" command. func SearchCommand() { - // Define your custom usage function first flag.Usage = func() { helpMessage := styling.HelpBorder.Render( "Search for a resource by name or partial match.\n\n", @@ -33,7 +31,6 @@ func SearchCommand() { return } - // Validate arguments before launching TUI if err := utils.ValidateSearchArgs(os.Args); err != nil { fmt.Println(err.Error()) os.Exit(1) diff --git a/cmd/types/types.go b/cmd/types/types.go index 2d7caef..99cf401 100644 --- a/cmd/types/types.go +++ b/cmd/types/types.go @@ -29,11 +29,13 @@ func TypesCommand() (string, error) { flag.Parse() + // Handle help flag if len(os.Args) == 3 && (os.Args[2] == "-h" || os.Args[2] == "--help") { flag.Usage() return output.String(), nil } + // Validate arguments if err := utils.ValidateTypesArgs(os.Args); err != nil { output.WriteString(err.Error()) return output.String(), err @@ -46,43 +48,59 @@ func TypesCommand() (string, error) { } type model struct { + quitting bool table table.Model selectedOption string } -func (m model) Init() tea.Cmd { return nil } +// Init initializes the model +func (m model) Init() tea.Cmd { + return nil +} +// Update handles user input and updates the model state func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var bubbleCmd tea.Cmd + switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "esc", "ctrl+c": + m.quitting = true return m, tea.Quit case "enter": + // User selected a type m.selectedOption = m.table.SelectedRow()[0] return m, tea.Quit } } + // Handle other updates (like navigation) m.table, bubbleCmd = m.table.Update(msg) return m, bubbleCmd } +// View renders the current UI func (m model) View() string { + if m.quitting { + return "\n Goodbye! \n" + } + + // Don't render anything if a selection has been made if m.selectedOption != "" { return "" } + // Render the type selection table with instructions return fmt.Sprintf("Select a type!\n%s\n%s", styling.TypesTableBorder.Render(m.table.View()), styling.KeyMenu.Render("↑ (move up) • ↓ (move down)\nenter (select) • ctrl+c | esc (quit)")) } // Function that generates and handles the type selection table -func tableGeneration(endpoint string) table.Model { +func tableGeneration(endpoint string) { types := []string{"Normal", "Fire", "Water", "Electric", "Grass", "Ice", - "Fighting", "Poison", "Ground", "Flying", "Psychic", "Bug", + "Fighting", "Poison", "Ground", "Flying", "Psychic", "Bug", "Dark", "Rock", "Ghost", "Dragon", "Steel", "Fairy"} rows := make([]table.Row, len(types)) @@ -90,6 +108,7 @@ func tableGeneration(endpoint string) table.Model { rows[i] = []string{t} } + // Initialize table with configuration t := table.New( table.WithColumns([]table.Column{{Title: "Type", Width: 16}}), table.WithRows(rows), @@ -97,6 +116,7 @@ func tableGeneration(endpoint string) table.Model { table.WithHeight(7), ) + // Set table styles s := table.DefaultStyles() s.Header = s.Header. BorderStyle(lipgloss.NormalBorder()). @@ -108,12 +128,15 @@ func tableGeneration(endpoint string) table.Model { t.SetStyles(s) m := model{table: t} - if programModel, err := tea.NewProgram(m).Run(); err != nil { + programModel, err := tea.NewProgram(m).Run() + + if err != nil { fmt.Println("Error running program:", err) os.Exit(1) - } else if finalModel, ok := programModel.(model); ok && finalModel.selectedOption != "quit" { - DamageTable(strings.ToLower(finalModel.selectedOption), endpoint) } - return t + // Only show damage table if a type was actually selected (not when quitting) + if finalModel, ok := programModel.(model); ok && finalModel.selectedOption != "" { + DamageTable(strings.ToLower(finalModel.selectedOption), endpoint) + } } diff --git a/cmd/types/types_test.go b/cmd/types/types_test.go index 928404b..ccef786 100644 --- a/cmd/types/types_test.go +++ b/cmd/types/types_test.go @@ -1,11 +1,16 @@ package types import ( + "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/x/exp/teatest" "github.com/digitalghost-dev/poke-cli/cmd/utils" "github.com/digitalghost-dev/poke-cli/styling" "github.com/stretchr/testify/assert" "os" "testing" + "time" ) func TestTypesCommand(t *testing.T) { @@ -45,3 +50,110 @@ func TestModelInit(t *testing.T) { cmd := m.Init() assert.Nil(t, cmd, "Init() should return nil") } + +// createTestModel creates a model with a table for testing +func createTestModel() model { + // Create a simple table with a few types + types := []string{"Normal", "Fire", "Water"} + rows := make([]table.Row, len(types)) + for i, t := range types { + rows[i] = []string{t} + } + + t := table.New( + table.WithColumns([]table.Column{{Title: "Type", Width: 16}}), + table.WithRows(rows), + table.WithFocused(true), + table.WithHeight(7), + ) + + // Set table styles + s := table.DefaultStyles() + s.Header = s.Header. + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("#FFCC00")). + BorderBottom(true) + s.Selected = s.Selected. + Foreground(lipgloss.Color("#000")). + Background(lipgloss.Color("#FFCC00")) + t.SetStyles(s) + + return model{table: t} +} + +func TestUpdate(t *testing.T) { + t.Run("Escape key should set quitting to true", func(t *testing.T) { + m := createTestModel() + testModel := teatest.NewTestModel(t, m) + + // Send escape key + testModel.Send(tea.KeyMsg{Type: tea.KeyEsc}) + testModel.WaitFinished(t, teatest.WithFinalTimeout(300*time.Millisecond)) + + final := testModel.FinalModel(t).(model) + assert.True(t, final.quitting, "quitting should be true after pressing escape") + }) + + t.Run("Ctrl+C key should set quitting to true", func(t *testing.T) { + m := createTestModel() + testModel := teatest.NewTestModel(t, m) + + // Send ctrl+c key + testModel.Send(tea.KeyMsg{Type: tea.KeyCtrlC}) + testModel.WaitFinished(t, teatest.WithFinalTimeout(300*time.Millisecond)) + + final := testModel.FinalModel(t).(model) + assert.True(t, final.quitting, "quitting should be true after pressing ctrl+c") + }) + + t.Run("Enter key should set selectedOption", func(t *testing.T) { + m := createTestModel() + testModel := teatest.NewTestModel(t, m) + + // Send enter key + testModel.Send(tea.KeyMsg{Type: tea.KeyEnter}) + testModel.WaitFinished(t, teatest.WithFinalTimeout(300*time.Millisecond)) + + final := testModel.FinalModel(t).(model) + assert.Equal(t, "Normal", final.selectedOption, "selectedOption should be set to the selected row") + }) + + t.Run("Arrow keys should update table selection", func(t *testing.T) { + m := createTestModel() + testModel := teatest.NewTestModel(t, m) + + // Send down arrow key to select the second row + testModel.Send(tea.KeyMsg{Type: tea.KeyDown}) + + // Then send enter to select it + testModel.Send(tea.KeyMsg{Type: tea.KeyEnter}) + testModel.WaitFinished(t, teatest.WithFinalTimeout(300*time.Millisecond)) + + final := testModel.FinalModel(t).(model) + assert.Equal(t, "Fire", final.selectedOption, "selectedOption should be updated after arrow navigation") + }) +} + +func TestView(t *testing.T) { + t.Run("View should return goodbye message when quitting", func(t *testing.T) { + m := createTestModel() + m.quitting = true + + view := m.View() + assert.Equal(t, "\n Goodbye! \n", view, "View should return goodbye message when quitting") + }) + + t.Run("View should return empty string when selectedOption is set", func(t *testing.T) { + m := createTestModel() + m.selectedOption = "Fire" + }) + + t.Run("View should render table in normal state", func(t *testing.T) { + m := createTestModel() + + view := m.View() + assert.Contains(t, view, "Select a type!", "View should contain the title") + assert.Contains(t, view, "Type", "View should contain the table header") + assert.Contains(t, view, "move up", "View should contain the key menu") + }) +} diff --git a/cmd/utils/validateargs.go b/cmd/utils/validateargs.go index 3df37ae..dce7cfe 100644 --- a/cmd/utils/validateargs.go +++ b/cmd/utils/validateargs.go @@ -6,7 +6,7 @@ import ( "strings" ) -// checkLength checks if the number of arguments is lower than the max value +// checkLength checks if the number of arguments is lower than the max value. Helper Function. func checkLength(args []string, max int) error { if len(args) > max { errMessage := styling.ErrorBorder.Render( @@ -17,6 +17,16 @@ func checkLength(args []string, max int) error { return nil } +// checkNoOtherOptions checks if there are exactly 3 arguments and the third argument is neither '-h' nor '--help' +func checkNoOtherOptions(args []string, max int, commandName string) error { + if len(args) == max && args[2] != "-h" && args[2] != "--help" { + errMsg := styling.ErrorColor.Render("Error!") + + "\nThe only available options after the\n" + commandName + " command are '-h' or '--help'" + return fmt.Errorf("%s", styling.ErrorBorder.Render(errMsg)) + } + return nil +} + // ValidateAbilityArgs validates the command line arguments func ValidateAbilityArgs(args []string) error { if err := checkLength(args, 4); err != nil { @@ -31,6 +41,20 @@ func ValidateAbilityArgs(args []string) error { return nil } +// ValidateItemArgs validates the command line arguments +func ValidateItemArgs(args []string) error { + if err := checkLength(args, 3); err != nil { + return err + } + + if len(args) == 2 { + errMessage := styling.ErrorBorder.Render(styling.ErrorColor.Render("Error!"), "\nPlease specify an item ") + return fmt.Errorf("%s", errMessage) + } + + return nil +} + // ValidateMoveArgs validates the command line arguments func ValidateMoveArgs(args []string) error { if err := checkLength(args, 3); err != nil { @@ -38,7 +62,7 @@ func ValidateMoveArgs(args []string) error { } if len(args) == 2 { - errMessage := styling.ErrorBorder.Render(styling.ErrorColor.Render("Error!"), "\nPlease specify a move") + errMessage := styling.ErrorBorder.Render(styling.ErrorColor.Render("Error!"), "\nPlease specify a move ") return fmt.Errorf("%s", errMessage) } @@ -51,12 +75,8 @@ func ValidateNaturesArgs(args []string) error { return err } - // Check if there are exactly 3 arguments and the third argument is neither '-h' nor '--help' - // If true, return an error message since only '-h' and '--help' are allowed after 'types' - if len(args) == 3 && args[2] != "-h" && args[2] != "--help" { - errMsg := styling.ErrorColor.Render("Error!") + - "\nThe only currently available options\nafter command are '-h' or '--help'" - return fmt.Errorf("%s", styling.ErrorBorder.Render(errMsg)) + if err := checkNoOtherOptions(args, 3, ""); err != nil { + return err } return nil @@ -101,11 +121,11 @@ func ValidatePokemonArgs(args []string) error { // Validate each argument after the Pokémon's name if len(args) > 3 { for _, arg := range args[3:] { - // Check for single `-` or `--` which are invalid + // Check for an empty flag after Pokémon's name if arg == "-" || arg == "--" { errorTitle := styling.ErrorColor.Render("Error!") errorString := fmt.Sprintf( - "\nInvalid argument '%s'. Single '-' or '--' is not allowed.\nPlease use valid flags.", + "\nEmpty flag '%s'.\nPlease specify valid flag(s).", arg, ) finalErrorMessage := errorTitle + errorString @@ -113,7 +133,7 @@ func ValidatePokemonArgs(args []string) error { return fmt.Errorf("%s", renderedError) } - // Check if the argument starts with a flag prefix but is invalid + // Check if the argument after Pokémon's name is an attempted flag if arg[0] != '-' { errorTitle := styling.ErrorColor.Render("Error!") errorString := fmt.Sprintf( @@ -136,12 +156,8 @@ func ValidateSearchArgs(args []string) error { return err } - // Check if there are exactly 3 arguments and the third argument is neither '-h' nor '--help' - // If true, return an error message since only '-h' and '--help' are allowed after - if len(args) == 3 && args[2] != "-h" && args[2] != "--help" { - errMsg := styling.ErrorColor.Render("Error!") + - "\nThe only currently available options\nafter command are '-h' or '--help'" - return fmt.Errorf("%s", styling.ErrorBorder.Render(errMsg)) + if err := checkNoOtherOptions(args, 3, ""); err != nil { + return err } return nil @@ -153,12 +169,8 @@ func ValidateTypesArgs(args []string) error { return err } - // Check if there are exactly 3 arguments and the third argument is neither '-h' nor '--help' - // If true, return an error message since only '-h' and '--help' are allowed after - if len(args) == 3 && args[2] != "-h" && args[2] != "--help" { - errMsg := styling.ErrorColor.Render("Error!") + - "\nThe only currently available options\nafter command are '-h' or '--help'" - return fmt.Errorf("%s", styling.ErrorBorder.Render(errMsg)) + if err := checkNoOtherOptions(args, 3, ""); err != nil { + return err } return nil diff --git a/connections/connection.go b/connections/connection.go index 9bf726c..f4bca9e 100644 --- a/connections/connection.go +++ b/connections/connection.go @@ -66,6 +66,24 @@ func AbilityApiCall(endpoint string, abilityName string, baseURL string) (struct return abilityStruct, abilityStruct.Name, nil } +// 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 +} + // 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 diff --git a/connections/connection_test.go b/connections/connection_test.go index fcb19d4..dc4b186 100644 --- a/connections/connection_test.go +++ b/connections/connection_test.go @@ -95,6 +95,43 @@ func TestAbilityApiCall(t *testing.T) { }) } +func TestItemApiCall(t *testing.T) { + t.Run("Successful API call returns expected item", func(t *testing.T) { + expectedItem := structs.ItemJSONStruct{ + Name: "choice-band", + } + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(expectedItem) + assert.NoError(t, err, "Expected no error for encoding response") + })) + defer ts.Close() + + item, name, err := ItemApiCall("/item", "choice-band", ts.URL) + + require.NoError(t, err, "Expected no error on successful API call") + assert.Equal(t, expectedItem, item, "Expected item struct does not match") + assert.Equal(t, "choice-band", name, "Expected item name does not match") + }) + + 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() + + item, _, 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.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") + }) +} + func TestMoveApiCall(t *testing.T) { t.Run("Successful API call returns expected move", func(t *testing.T) { expectedMove := structs.MoveJSONStruct{ diff --git a/docs/assets/item.gif b/docs/assets/item.gif new file mode 100644 index 0000000..d5b6146 Binary files /dev/null and b/docs/assets/item.gif differ diff --git a/docs/commands.md b/docs/commands.md index 04008cc..2610a02 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -31,6 +31,20 @@ Output: --- +## `item` +* Retrieve information about a specific item, including its cost, category and description. + +Example: +```console +$ poke-cli item poke-ball +``` + +Output: + +![item_command](assets/item.gif) + +--- + ## `move` * Retrieve information about a specific move, including its type, power, PP, accuracy, category, etc., and the move's effect. diff --git a/flags/pokemonflagset_test.go b/flags/pokemonflagset_test.go index 91e34ed..cbc3d85 100644 --- a/flags/pokemonflagset_test.go +++ b/flags/pokemonflagset_test.go @@ -80,6 +80,55 @@ Hidden Ability: Chlorophyll assert.Equal(t, expectedOutput, actualOutput, "Output should contain data for the abilities flag") } +func TestMovesFlag(t *testing.T) { + // Capture standard output + var output bytes.Buffer + stdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + // Call the MovesFlag function with a valid Pokémon + err := MovesFlag(&output, "pokemon", "bulbasaur") + + // Close and restore stdout + if closeErr := w.Close(); closeErr != nil { + t.Fatalf("Failed to close pipe writer: %v", closeErr) + } + os.Stdout = stdout + + _, readErr := output.ReadFrom(r) + if readErr != nil { + t.Fatalf("Failed to read from pipe: %v", readErr) + } + + // Assert no errors occurred + require.NoError(t, err, "MovesFlag should not return an error for a valid Pokémon") + + // Get the actual output and strip ANSI codes + actualOutput := styling.StripANSI(output.String()) + + // Check for expected header + assert.Contains(t, actualOutput, "Learnable Moves", "Output should contain the header 'Learnable Moves'") + + // Check for table headers + assert.Contains(t, actualOutput, "Name", "Output should contain the 'Name' column header") + assert.Contains(t, actualOutput, "Level", "Output should contain the 'Level' column header") + assert.Contains(t, actualOutput, "Type", "Output should contain the 'Type' column header") + assert.Contains(t, actualOutput, "Accuracy", "Output should contain the 'Accuracy' column header") + assert.Contains(t, actualOutput, "Power", "Output should contain the 'Power' column header") + + // Since the actual moves might change with API updates, we'll just check that the output is not empty + assert.NotEmpty(t, actualOutput, "Output should not be empty") + + // Check that the output contains some common moves for Bulbasaur + // Note: These are common moves, but if they change in the API, this test might need updating + assert.True(t, + strings.Contains(actualOutput, "Tackle") || + strings.Contains(actualOutput, "Vine Whip") || + strings.Contains(actualOutput, "Growl"), + "Output should contain at least one of Bulbasaur's common moves") +} + func TestImageFlag(t *testing.T) { // Capture standard output var output bytes.Buffer diff --git a/go.mod b/go.mod index 18cf049..124b266 100644 --- a/go.mod +++ b/go.mod @@ -4,23 +4,23 @@ go 1.24.4 require ( github.com/charmbracelet/bubbles v0.20.0 - github.com/charmbracelet/bubbletea v1.2.4 + github.com/charmbracelet/bubbletea v1.3.5 github.com/charmbracelet/lipgloss v1.1.0 - github.com/charmbracelet/x/exp/teatest v0.0.0-20250317102001-c803e5cafd0b + github.com/charmbracelet/x/exp/teatest v0.0.0-20250702191427-5bdfc8f2e4ff github.com/charmbracelet/x/term v0.2.1 github.com/disintegration/imaging v1.6.2 github.com/stretchr/testify v1.10.0 - golang.org/x/text v0.21.0 + golang.org/x/text v0.26.0 ) require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/aymanbagabas/go-udiff v0.2.0 // indirect - github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect - github.com/charmbracelet/x/ansi v0.8.0 // indirect - github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect - github.com/charmbracelet/x/exp/golden v0.0.0-20241212170349-ad4b7ae0f25f // indirect + github.com/aymanbagabas/go-udiff v0.3.1 // indirect + github.com/charmbracelet/colorprofile v0.3.1 // indirect + github.com/charmbracelet/x/ansi v0.9.3 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13 // indirect + github.com/charmbracelet/x/exp/golden v0.0.0-20250702191427-5bdfc8f2e4ff // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect @@ -34,8 +34,12 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect - golang.org/x/image v0.23.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.30.0 // indirect + golang.org/x/image v0.28.0 // indirect + golang.org/x/sync v0.15.0 // indirect + golang.org/x/sys v0.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +// v1.3.4 was pushed as test and not a real version. +// The release was then deleted on GitHub. v1.4.0 follows. +retract v1.3.4 diff --git a/go.sum b/go.sum index c0fbb3b..e6a6aa2 100644 --- a/go.sum +++ b/go.sum @@ -2,24 +2,24 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= -github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= +github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= -github.com/charmbracelet/bubbletea v1.2.4 h1:KN8aCViA0eps9SCOThb2/XPIlea3ANJLUkv3KnQRNCE= -github.com/charmbracelet/bubbletea v1.2.4/go.mod h1:Qr6fVQw+wX7JkWWkVyXYk/ZUQ92a6XNekLXa3rR18MM= -github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= -github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc= +github.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54= +github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40= +github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= -github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= -github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= -github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= -github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= -github.com/charmbracelet/x/exp/golden v0.0.0-20241212170349-ad4b7ae0f25f h1:UytXHv0UxnsDFmL/7Z9Q5SBYPwSuRLXHbwx+6LycZ2w= -github.com/charmbracelet/x/exp/golden v0.0.0-20241212170349-ad4b7ae0f25f/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= -github.com/charmbracelet/x/exp/teatest v0.0.0-20250317102001-c803e5cafd0b h1:stRYpFhKYr6u2DrdyXHabpXQdRbNs8REQ4RB/Kc22t0= -github.com/charmbracelet/x/exp/teatest v0.0.0-20250317102001-c803e5cafd0b/go.mod h1:ag+SpTUkiN/UuUGYPX3Ci4fR1oF3XX97PpGhiXK7i6U= +github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0= +github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= +github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= +github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/exp/golden v0.0.0-20250702191427-5bdfc8f2e4ff h1:mEllIwjDs9aKqnzckYmNZqxoULwp4afFLVgH9x9QAGA= +github.com/charmbracelet/x/exp/golden v0.0.0-20250702191427-5bdfc8f2e4ff/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= +github.com/charmbracelet/x/exp/teatest v0.0.0-20250702191427-5bdfc8f2e4ff h1:DKxPeQDSnQPDxD27Bq8nUDwiSecy2Yf+nT3U/TlPXs8= +github.com/charmbracelet/x/exp/teatest v0.0.0-20250702191427-5bdfc8f2e4ff/go.mod h1:RXbDhep1qKL/SEz2IuOhOUrsNHDKGqRmGks1nZStKyU= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -54,17 +54,17 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJu golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= -golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE= +golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/structs/structs.go b/structs/structs.go index 5a650bd..6cb083c 100644 --- a/structs/structs.go +++ b/structs/structs.go @@ -35,6 +35,31 @@ type AbilityJSONStruct struct { } `json:"pokemon"` } +// ItemJSONStruct item endpoint from API +type ItemJSONStruct struct { + Name string `json:"name"` + ID int `json:"id"` + Cost int `json:"cost"` + Category struct { + Name string `json:"name"` + URL string `json:"url"` + } `json:"category"` + FlavorTextEntries []struct { + Language struct { + Name string `json:"name"` + URL string `json:"url"` + } `json:"language"` + Text string `json:"text"` + VersionGroup struct { + Name string `json:"name"` + URL string `json:"url"` + } `json:"version_group"` + } `json:"flavor_text_entries"` + Sprites struct { + Default string `json:"default"` + } `json:"sprites"` +} + // MoveJSONStruct move endpoint from API type MoveJSONStruct struct { Name string `json:"name"` diff --git a/testdata/cli_help.golden b/testdata/cli_help.golden index 43bc33d..73ec9eb 100644 --- a/testdata/cli_help.golden +++ b/testdata/cli_help.golden @@ -13,6 +13,7 @@ │ │ │ COMMANDS: │ │ ability Get details about an ability │ +│ item Get details about an item │ │ move Get details about a move │ │ natures Get details about all natures │ │ pokemon Get details about a Pokémon │ diff --git a/testdata/item.golden b/testdata/item.golden new file mode 100644 index 0000000..f9438c5 --- /dev/null +++ b/testdata/item.golden @@ -0,0 +1,14 @@ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ ┃ +┃ Choice Band ┃ +┃ Cost: 4000 ┃ +┃ Category: Choice ┃ +┃ --- ┃ +┃ Description: ┃ +┃ An item to be held by a ┃ +┃ Pokémon. This curious ┃ +┃ headband boosts Attack but ┃ +┃ only allows the use of ┃ +┃ one move. ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ diff --git a/testdata/item_help.golden b/testdata/item_help.golden new file mode 100644 index 0000000..c197516 --- /dev/null +++ b/testdata/item_help.golden @@ -0,0 +1,10 @@ +╭────────────────────────────────────────────────────────╮ +│Get details about a specific item. │ +│ │ +│ USAGE: │ +│ poke-cli item [flag] │ +│ Use a hyphen when typing a name with a space. │ +│ │ +│ FLAGS: │ +│ -h, --help Prints the help menu.│ +╰────────────────────────────────────────────────────────╯ \ No newline at end of file diff --git a/testdata/item_missing_data.golden b/testdata/item_missing_data.golden new file mode 100644 index 0000000..58aca70 --- /dev/null +++ b/testdata/item_missing_data.golden @@ -0,0 +1,10 @@ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ ┃ +┃ Clear Amulet ┃ +┃ Cost: 30000 ┃ +┃ Category: Held Items ┃ +┃ --- ┃ +┃ Description: ┃ +┃ Missing data from API ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ diff --git a/testdata/item_too_many_args.golden b/testdata/item_too_many_args.golden new file mode 100644 index 0000000..093717c --- /dev/null +++ b/testdata/item_too_many_args.golden @@ -0,0 +1,4 @@ +╭──────────────────╮ +│Error! │ +│Too many arguments│ +╰──────────────────╯ \ No newline at end of file diff --git a/testdata/natures_invalid_extra_arg.golden b/testdata/natures_invalid_extra_arg.golden index de50ee1..93450ba 100644 --- a/testdata/natures_invalid_extra_arg.golden +++ b/testdata/natures_invalid_extra_arg.golden @@ -1,5 +1,5 @@ -╭────────────────────────────────────────────╮ -│Error! │ -│The only currently available options │ -│after command are '-h' or '--help'│ -╰────────────────────────────────────────────╯ \ No newline at end of file +╭──────────────────────────────────────╮ +│Error! │ +│The only available options after the │ +│ command are '-h' or '--help'│ +╰──────────────────────────────────────╯ \ No newline at end of file diff --git a/testdata/pokemon_image_flag_empty_flag.golden b/testdata/pokemon_image_flag_empty_flag.golden new file mode 100644 index 0000000..ec831c7 --- /dev/null +++ b/testdata/pokemon_image_flag_empty_flag.golden @@ -0,0 +1,5 @@ +╭─────────────────────────────╮ +│Error! │ +│Empty flag '--'. │ +│Please specify valid flag(s).│ +╰─────────────────────────────╯ \ No newline at end of file diff --git a/testdata/pokemon_invalid_image_flag.golden b/testdata/pokemon_image_flag_missing_size.golden similarity index 100% rename from testdata/pokemon_invalid_image_flag.golden rename to testdata/pokemon_image_flag_missing_size.golden diff --git a/testdata/pokemon_image_flag_non-valid_size.golden b/testdata/pokemon_image_flag_non-valid_size.golden new file mode 100644 index 0000000..d74aaea --- /dev/null +++ b/testdata/pokemon_image_flag_non-valid_size.golden @@ -0,0 +1,11 @@ +Your selected Pokémon: Floatzel +• National Pokédex #: 419 +• Weight: 33.5kg (73.9 lbs) +• Height: 3.6m (3′07″) +───── +Image +╭───────────────────────────╮ +│Error! │ +│Invalid image size. │ +│Valid sizes are: lg, md, sm│ +╰───────────────────────────╯