diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e8ea271..86e8c1b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -31,7 +31,7 @@ on:
- main
env:
- VERSION_NUMBER: 'v1.8.5'
+ VERSION_NUMBER: 'v1.8.6'
DOCKERHUB_REGISTRY_NAME: 'digitalghostdev/poke-cli'
AWS_REGION: 'us-west-2'
diff --git a/.gitignore b/.gitignore
index 0fb4852..57a8688 100644
--- a/.gitignore
+++ b/.gitignore
@@ -67,3 +67,4 @@ card_data/storage/
CLAUDE.md
REFACTORING.md
/card_data/.codspeed/
+/.ai/
diff --git a/.goreleaser.yml b/.goreleaser.yml
index 0918c3a..c1b0403 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -14,7 +14,7 @@ builds:
- windows
- darwin
ldflags:
- - -s -w -X main.version=v1.8.5
+ - -s -w -X main.version=v1.8.6
archives:
- formats: [ 'zip' ]
diff --git a/Dockerfile b/Dockerfile
index 29f2917..2d12483 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -8,7 +8,7 @@ RUN go mod download
COPY . .
-RUN go build -ldflags "-X main.version=v1.8.5" -o poke-cli .
+RUN go build -ldflags "-X main.version=v1.8.6" -o poke-cli .
# build 2
FROM --platform=$BUILDPLATFORM alpine:3.23
diff --git a/README.md b/README.md
index e3e7afc..0c6ff71 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
-
+
@@ -96,11 +96,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.5 [subcommand] [flag]
+ docker run --rm -it digitalghostdev/poke-cli:v1.8.6 [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.5 -c "cd /app && exec sh"
+ docker run --rm -it --name poke-cli --entrypoint /bin/sh digitalghostdev/poke-cli:v1.8.6 -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/README.md b/card_data/README.md
index 9017a98..2ffd2d9 100644
--- a/card_data/README.md
+++ b/card_data/README.md
@@ -7,7 +7,7 @@ and decided to process all the data myself, load it into Supabase, and read from
## Data Architecture
Runs at 2:00PM PST daily.
-
+
1. TCGPlayer pricing data and TCGDex card data are called and processed through a data pipeline orchestrated by Dagster and hosted on AWS.
diff --git a/card_data/data_infrastructure_v2.png b/card_data/data_infrastructure_v2.png
new file mode 100644
index 0000000..cae98e9
Binary files /dev/null and b/card_data/data_infrastructure_v2.png differ
diff --git a/card_data/pipelines/poke_cli_dbt/dbt_project.yml b/card_data/pipelines/poke_cli_dbt/dbt_project.yml
index d4cd880..46f6ddf 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.5'
+version: '1.8.6'
profile: 'poke_cli_dbt'
diff --git a/card_data/pipelines/poke_cli_dbt/macros/create_view.sql b/card_data/pipelines/poke_cli_dbt/macros/create_view.sql
index 5e940f1..ba0a52d 100644
--- a/card_data/pipelines/poke_cli_dbt/macros/create_view.sql
+++ b/card_data/pipelines/poke_cli_dbt/macros/create_view.sql
@@ -10,7 +10,8 @@
"localId",
"set_cardCount_official",
CONCAT(name, ' - ', LPAD("localId", 3, '0'), '/', LPAD("set_cardCount_official"::text, 3, '0')) AS card_combined_name,
- set_name
+ set_name,
+ regulation_mark
FROM public.cards
),
cards_pricing_cte AS (
@@ -30,7 +31,8 @@
LPAD(c."localId", 3, '0') AS "localId",
p."market_price",
COALESCE(p."card_number", LPAD(c."localId", 3, '0')) AS card_number,
- c.illustrator
+ c.illustrator,
+ c.regulation_mark
FROM
cards_cte AS c
LEFT JOIN
diff --git a/card_data/pipelines/poke_cli_dbt/models/cards.sql b/card_data/pipelines/poke_cli_dbt/models/cards.sql
index b908778..c49bbe4 100644
--- a/card_data/pipelines/poke_cli_dbt/models/cards.sql
+++ b/card_data/pipelines/poke_cli_dbt/models/cards.sql
@@ -3,5 +3,6 @@
post_hook="{{ enable_rls() }}"
) }}
-SELECT id, set_id, image, name, "localId", category, hp, "set_cardCount_official", set_name, illustrator
-FROM {{ source('staging', 'cards') }}
\ No newline at end of file
+SELECT id, set_id, image, name, "localId", category, hp, "set_cardCount_official", set_name, illustrator, "regulationMark" AS regulation_mark
+FROM {{ source('staging', 'cards') }}
+WHERE "localId" ~ '^[0-9]+$'
\ No newline at end of file
diff --git a/card_data/pyproject.toml b/card_data/pyproject.toml
index f257343..3e1a108 100644
--- a/card_data/pyproject.toml
+++ b/card_data/pyproject.toml
@@ -10,8 +10,8 @@ dependencies = [
"dagster>=1.11.3",
"dagster-dbt>=0.27.3",
"dagster-dg-cli>=1.11.3",
- "dagster-postgres>=0.27.3",
- "dagster-webserver>=1.11.3",
+ "dagster-postgres==0.28.7",
+ "dagster-webserver==1.12.7",
"dbt-core>=1.10.8",
"dbt-postgres>=1.9.0",
"pandas>=2.3.1",
@@ -27,10 +27,10 @@ dependencies = [
[dependency-groups]
dev = [
- "dagster-webserver",
+ "dagster-webserver==1.12.7",
"dagster-dg-cli",
"dagster-dbt>=0.27.3",
- "dagster-postgres>=0.27.3",
+ "dagster-postgres==0.28.7",
"pytest>=9.0.2",
"pytest-codspeed>=4.2.0",
"responses>=0.25.8",
diff --git a/card_data/uv.lock b/card_data/uv.lock
index dcc428a..39d1cf6 100644
--- a/card_data/uv.lock
+++ b/card_data/uv.lock
@@ -180,8 +180,8 @@ requires-dist = [
{ name = "dagster", specifier = ">=1.11.3" },
{ name = "dagster-dbt", specifier = ">=0.27.3" },
{ name = "dagster-dg-cli", specifier = ">=1.11.3" },
- { name = "dagster-postgres", specifier = ">=0.27.3" },
- { name = "dagster-webserver", specifier = ">=1.11.3" },
+ { name = "dagster-postgres", specifier = "==0.28.7" },
+ { name = "dagster-webserver", specifier = "==1.12.7" },
{ name = "dbt-core", specifier = ">=1.10.8" },
{ name = "dbt-postgres", specifier = ">=1.9.0" },
{ name = "pandas", specifier = ">=2.3.1" },
diff --git a/cli.go b/cli.go
index 8cc8502..39cfd47 100644
--- a/cli.go
+++ b/cli.go
@@ -24,6 +24,30 @@ import (
var version = "(devel)"
+var commandDescriptions = []struct {
+ name string
+ desc string
+}{
+ {"ability", "Get details about an ability"},
+ {"berry", "Get details about a berry"},
+ {"card", "Get details about a TCG card"},
+ {"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"},
+ {"search", "Search for a resource"},
+ {"speed", "Calculate the speed of a Pokémon in battle"},
+ {"types", "Get details about a typing"},
+}
+
+func renderCommandList() string {
+ var sb strings.Builder
+ for _, cmd := range commandDescriptions {
+ sb.WriteString(fmt.Sprintf("\n\t%-15s %s", cmd.name, cmd.desc))
+ }
+ return sb.String()
+}
+
func currentVersion() {
if version != "(devel)" {
// Use version injected by -ldflags
@@ -70,16 +94,7 @@ func runCLI(args []string) int {
fmt.Sprintf("\n\t%-15s %s", "-l, --latest", "Prints the latest version available"),
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", "berry", "Get details about a berry"),
- fmt.Sprintf("\n\t%-15s %s", "card", "Get details about a TCG card"),
- 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"),
- fmt.Sprintf("\n\t%-15s %s", "search", "Search for a resource"),
- fmt.Sprintf("\n\t%-15s %s", "speed", "Calculate the speed of a Pokémon in battle"),
- fmt.Sprintf("\n\t%-15s %s", "types", "Get details about a typing"),
+ renderCommandList(),
"\n\n", styling.StyleItalic.Render(styling.HyphenHint),
"\n", styling.StyleItalic.Render("example: poke-cli ability strong-jaw"),
"\n", styling.StyleItalic.Render("example: poke-cli pokemon flutter-mane"),
@@ -151,16 +166,7 @@ func runCLI(args []string) int {
styling.ErrorColor.Render("✖ Error!"),
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", "berry", "Get details about a berry"),
- fmt.Sprintf("\n\t%-15s %s", "card", "Get details about a TCG card"),
- 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"),
- fmt.Sprintf("\n\t%-15s %s", "search", "Search for a resource"),
- fmt.Sprintf("\n\t%-15s %s", "speed", "Calculate the speed of a Pokémon in battle"),
- fmt.Sprintf("\n\t%-15s %s", "types", "Get details about a typing"),
+ renderCommandList(),
fmt.Sprintf("\n\nAlso run %s for more info!", styling.StyleBold.Render("poke-cli -h")),
)
output.WriteString(errMessage)
diff --git a/cmd/ability/ability.go b/cmd/ability/ability.go
index e8ec795..76d8e30 100644
--- a/cmd/ability/ability.go
+++ b/cmd/ability/ability.go
@@ -99,8 +99,7 @@ func AbilityCommand() (string, error) {
if *af.Pokemon || *af.ShortPokemon {
if err := flags.PokemonAbilitiesFlag(&output, endpoint, abilityName); err != nil {
- output.WriteString(fmt.Sprintf("error parsing flags: %v\n", err))
- return output.String(), fmt.Errorf("error parsing flags: %w", err)
+ return utils.HandleFlagError(&output, err)
}
}
diff --git a/cmd/card/cardlist.go b/cmd/card/cardlist.go
index ba511e5..6627f1b 100644
--- a/cmd/card/cardlist.go
+++ b/cmd/card/cardlist.go
@@ -14,15 +14,16 @@ import (
)
type CardsModel struct {
- Choice string
- IllustratorMap map[string]string
- ImageMap map[string]string
- PriceMap map[string]string
- Quitting bool
- SelectedOption string
- SeriesName string
- Table table.Model
- ViewImage bool
+ Choice string
+ IllustratorMap map[string]string
+ ImageMap map[string]string
+ PriceMap map[string]string
+ RegulationMarkMap map[string]string
+ Quitting bool
+ SelectedOption string
+ SeriesName string
+ Table table.Model
+ ViewImage bool
}
func (m CardsModel) Init() tea.Cmd {
@@ -70,7 +71,8 @@ func (m CardsModel) View() string {
price = "Price: Not available"
}
illustrator := m.IllustratorMap[cardName]
- selectedCard = cardName + "\n---\n" + price + "\n---\n" + illustrator
+ regulationMark := m.RegulationMarkMap[cardName]
+ selectedCard = cardName + "\n---\n" + price + "\n---\n" + illustrator + "\n---\n" + regulationMark
}
leftPanel := styling.TypesTableBorder.Render(m.Table.View())
@@ -96,11 +98,12 @@ type cardData struct {
MarketPrice float64 `json:"market_price"`
Name string `json:"name"`
NumberPlusName string `json:"number_plus_name"`
+ RegulationMark string `json:"regulation_mark"`
}
// CardsList creates and returns a new CardsModel with cards from a specific set
func CardsList(setID string) (CardsModel, error) {
- url := fmt.Sprintf("https://uoddayfnfkebrijlpfbh.supabase.co/rest/v1/card_pricing_view?set_id=eq.%s&select=number_plus_name,market_price,image_url,illustrator&order=localId", setID)
+ url := fmt.Sprintf("https://uoddayfnfkebrijlpfbh.supabase.co/rest/v1/card_pricing_view?set_id=eq.%s&select=number_plus_name,market_price,image_url,illustrator,regulation_mark&order=localId", setID)
body, err := getCardData(url)
if err != nil {
return CardsModel{}, fmt.Errorf("failed to fetch card data: %w", err)
@@ -117,6 +120,7 @@ func CardsList(setID string) (CardsModel, error) {
priceMap := make(map[string]string)
imageMap := make(map[string]string)
illustratorMap := make(map[string]string)
+ regulationMarkMap := make(map[string]string)
for i, card := range allCards {
rows[i] = []string{card.NumberPlusName}
if card.MarketPrice != 0 {
@@ -131,6 +135,12 @@ func CardsList(setID string) (CardsModel, error) {
illustratorMap[card.NumberPlusName] = "Illustrator not available"
}
+ if card.RegulationMark != "" {
+ regulationMarkMap[card.NumberPlusName] = "Regulation: " + card.RegulationMark
+ } else {
+ regulationMarkMap[card.NumberPlusName] = "Regulation not available"
+ }
+
imageMap[card.NumberPlusName] = card.ImageURL
}
@@ -155,6 +165,7 @@ func CardsList(setID string) (CardsModel, error) {
IllustratorMap: illustratorMap,
ImageMap: imageMap,
PriceMap: priceMap,
+ RegulationMarkMap: regulationMarkMap,
Table: t,
}, nil
}
diff --git a/cmd/card/cardlist_test.go b/cmd/card/cardlist_test.go
index 36a2b17..e3c51e9 100644
--- a/cmd/card/cardlist_test.go
+++ b/cmd/card/cardlist_test.go
@@ -11,6 +11,10 @@ import (
tea "github.com/charmbracelet/bubbletea"
)
+// testSupabaseKey is the publishable API key used in tests.
+// Extracted to a constant for easier maintenance if the key changes.
+const testSupabaseKey = "sb_publishable_oondaaAIQC-wafhEiNgpSQ_reRiEp7j"
+
func TestCardsModel_Init(t *testing.T) {
model := CardsModel{
SeriesName: "sv",
@@ -305,10 +309,10 @@ func TestCardsList_EmptyResult(t *testing.T) {
func TestCallCardData_SendsHeadersAndReturnsBody(t *testing.T) {
// Start a test HTTP server that validates headers and returns a body
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- if got := r.Header.Get("apikey"); got != "sb_publishable_oondaaAIQC-wafhEiNgpSQ_reRiEp7j" {
+ if got := r.Header.Get("apikey"); got != testSupabaseKey {
t.Fatalf("missing or wrong apikey header: %q", got)
}
- if got := r.Header.Get("Authorization"); got != "Bearer sb_publishable_oondaaAIQC-wafhEiNgpSQ_reRiEp7j" {
+ if got := r.Header.Get("Authorization"); got != "Bearer "+testSupabaseKey {
t.Fatalf("missing or wrong Authorization header: %q", got)
}
if got := r.Header.Get("Content-Type"); got != "application/json" {
diff --git a/cmd/card/setslist_test.go b/cmd/card/setslist_test.go
index c1b0118..1fcdc80 100644
--- a/cmd/card/setslist_test.go
+++ b/cmd/card/setslist_test.go
@@ -192,10 +192,10 @@ func TestSetsModel_Update_EnterKey(t *testing.T) {
func TestCallSetsData_SendsHeadersAndReturnsBody(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- if got := r.Header.Get("apikey"); got != "sb_publishable_oondaaAIQC-wafhEiNgpSQ_reRiEp7j" {
+ if got := r.Header.Get("apikey"); got != testSupabaseKey {
t.Fatalf("missing or wrong apikey header: %q", got)
}
- if got := r.Header.Get("Authorization"); got != "Bearer sb_publishable_oondaaAIQC-wafhEiNgpSQ_reRiEp7j" {
+ if got := r.Header.Get("Authorization"); got != "Bearer "+testSupabaseKey {
t.Fatalf("missing or wrong Authorization header: %q", got)
}
if got := r.Header.Get("Content-Type"); got != "application/json" {
diff --git a/cmd/search/parse.go b/cmd/search/parse.go
index 50b7e7b..e92d51b 100644
--- a/cmd/search/parse.go
+++ b/cmd/search/parse.go
@@ -1,9 +1,12 @@
package search
import (
+ "fmt"
+ "regexp"
"strings"
"github.com/digitalghost-dev/poke-cli/connections"
+ "github.com/schollz/closestmatch"
)
type Resource struct {
@@ -19,40 +22,94 @@ type Result struct {
URL string `json:"url"`
}
-func parseSearch(results []Result, search string) []Result {
- var x int
- var substr string
-
- for _, result := range results {
- if string(search[0]) == "^" {
- substr = search[1:]
- if len(substr) > len(result.Name) {
- continue
- }
- if result.Name[0:len(substr)] != substr {
- continue
- }
- } else {
- if !strings.Contains(result.Name, search) {
- continue
- }
+func containsRegexChars(s string) bool {
+ return strings.ContainsAny(s, "^$.*+?[]{}()|\\")
+}
+
+func parseRegex(results []Result, pattern string) ([]Result, error) {
+ re, err := regexp.Compile(pattern)
+ if err != nil {
+ return nil, fmt.Errorf("invalid regex pattern: %w", err)
+ }
+
+ filtered := make([]Result, 0)
+ for _, r := range results {
+ if re.MatchString(r.Name) {
+ filtered = append(filtered, r)
+ }
+ }
+ return filtered, nil
+}
+
+func parseFuzzy(results []Result, search string) []Result {
+ name := make([]string, len(results))
+ for i, r := range results {
+ name[i] = r.Name
+ }
+
+ bagSizes := []int{2, 3, 4}
+ cm := closestmatch.New(name, bagSizes)
+
+ matches := cm.ClosestN(search, 10)
+
+ matchSet := make(map[string]struct{}, len(matches))
+ for _, m := range matches {
+ matchSet[m] = struct{}{}
+ }
+
+ filtered := make([]Result, 0, len(matches))
+ for _, r := range results {
+ if _, ok := matchSet[r.Name]; ok {
+ filtered = append(filtered, r)
+ }
+ }
+ return filtered
+}
+
+func parseContains(results []Result, search string) []Result {
+ needle := strings.ToLower(strings.TrimSpace(search))
+ if needle == "" {
+ return results
+ }
+
+ filtered := make([]Result, 0)
+ for _, r := range results {
+ if strings.Contains(strings.ToLower(r.Name), needle) {
+ filtered = append(filtered, r)
}
- results[x] = result
- x++
}
- return results[:x]
+ return filtered
+}
+
+func parseSearch(results []Result, search string) ([]Result, error) {
+ if strings.TrimSpace(search) == "" {
+ return results, nil
+ }
+
+ if containsRegexChars(search) {
+ return parseRegex(results, search)
+ }
+
+ if filtered := parseContains(results, search); len(filtered) > 0 {
+ return filtered, nil
+ }
+
+ return parseFuzzy(results, search), nil
}
var apiCall = connections.ApiCallSetup // set as a var for testability
-// Search returns resources list, filtered by resources term.
+// Search returns a resources list, filtered by resources term.
func query(endpoint string, search string) (result Resource, err error) {
url := connections.APIURL + endpoint + "/?offset=0&limit=9999"
err = apiCall(url, &result, false)
if err != nil {
return
}
- result.Results = parseSearch(result.Results, search)
+ result.Results, err = parseSearch(result.Results, search)
+ if err != nil {
+ return
+ }
result.Count = len(result.Results)
return
}
diff --git a/cmd/search/parse_test.go b/cmd/search/parse_test.go
index e3cd81b..2a0ac86 100644
--- a/cmd/search/parse_test.go
+++ b/cmd/search/parse_test.go
@@ -4,52 +4,178 @@ import "testing"
func TestParseSearch(t *testing.T) {
mockResults := []Result{
+ {Name: "hariyama"},
{Name: "pikachu"},
{Name: "raichu"},
{Name: "pidgey"},
{Name: "sandshrew"},
+ {Name: "bulbasaur"},
+ {Name: "charmander"},
+ {Name: "charmeleon"},
+ {Name: "squirtle"},
+ {Name: "musharna"},
+ {Name: "caterpie"},
+ {Name: "weedle"},
+ {Name: "rattata"},
}
- tests := []struct {
- name string
- search string
- expected []string
- }{
- {
- name: "Contains match",
- search: "chu",
- expected: []string{"pikachu", "raichu"},
- },
- {
- name: "Prefix match",
- search: "^pi",
- expected: []string{"pikachu", "pidgey"},
- },
- {
- name: "No match",
- search: "^z",
- expected: []string{},
- },
- {
- name: "Contains s",
- search: "s",
- expected: []string{"sandshrew"},
- },
- }
+ t.Run("Substring match prefers contains over fuzzy", func(t *testing.T) {
+ filtered, err := parseSearch(mockResults, "hari")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if len(filtered) != 1 {
+ t.Fatalf("expected 1 result, got %d", len(filtered))
+ }
+ if filtered[0].Name != "hariyama" {
+ t.Fatalf("expected hariyama, got %s", filtered[0].Name)
+ }
+ })
+
+ t.Run("Contains is case-insensitive and trims whitespace", func(t *testing.T) {
+ filtered, err := parseSearch(mockResults, " HARI ")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if len(filtered) != 1 {
+ t.Fatalf("expected 1 result, got %d", len(filtered))
+ }
+ if filtered[0].Name != "hariyama" {
+ t.Fatalf("expected hariyama, got %s", filtered[0].Name)
+ }
+ })
+
+ t.Run("Fuzzy matching is used when contains has no matches", func(t *testing.T) {
+ filtered, err := parseSearch(mockResults, "charmender")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if len(filtered) == 0 {
+ t.Fatalf("expected fuzzy results, got 0")
+ }
+
+ found := map[string]bool{}
+ for _, r := range filtered {
+ found[r.Name] = true
+ }
+ if !found["charmander"] {
+ t.Fatalf("expected fuzzy results to include charmander, got %#v", found)
+ }
+ })
- for _, tc := range tests {
- t.Run(tc.name, func(t *testing.T) {
- filtered := parseSearch(mockResults, tc.search)
- if len(filtered) != len(tc.expected) {
- t.Errorf("expected %d results, got %d", len(tc.expected), len(filtered))
+ t.Run("Regex prefix match ^", func(t *testing.T) {
+ filtered, err := parseSearch(mockResults, "^pi")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ expected := []string{"pikachu", "pidgey"}
+ if len(filtered) != len(expected) {
+ t.Fatalf("expected %d results, got %d", len(expected), len(filtered))
+ }
+ for i, result := range filtered {
+ if result.Name != expected[i] {
+ t.Errorf("expected %s, got %s", expected[i], result.Name)
}
- for i, result := range filtered {
- if result.Name != tc.expected[i] {
- t.Errorf("expected %s, got %s", tc.expected[i], result.Name)
- }
+ }
+ })
+
+ t.Run("Regex suffix match $", func(t *testing.T) {
+ filtered, err := parseSearch(mockResults, "chu$")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ expected := []string{"pikachu", "raichu"}
+ if len(filtered) != len(expected) {
+ t.Fatalf("expected %d results, got %d", len(expected), len(filtered))
+ }
+ for i, result := range filtered {
+ if result.Name != expected[i] {
+ t.Errorf("expected %s, got %s", expected[i], result.Name)
}
- })
- }
+ }
+ })
+
+ t.Run("Regex no match", func(t *testing.T) {
+ filtered, err := parseSearch(mockResults, "^z")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if len(filtered) != 0 {
+ t.Errorf("expected 0 results, got %d", len(filtered))
+ }
+ })
+
+ t.Run("Regex character class []", func(t *testing.T) {
+ filtered, err := parseSearch(mockResults, "^[rs]")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ expected := []string{"raichu", "sandshrew", "squirtle", "rattata"}
+ if len(filtered) != len(expected) {
+ t.Fatalf("expected %d results, got %d", len(expected), len(filtered))
+ }
+ for i, result := range filtered {
+ if result.Name != expected[i] {
+ t.Errorf("expected %s, got %s", expected[i], result.Name)
+ }
+ }
+ })
+
+ t.Run("Regex alternation |", func(t *testing.T) {
+ filtered, err := parseSearch(mockResults, "^(pikachu|bulbasaur)$")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ expected := []string{"pikachu", "bulbasaur"}
+ if len(filtered) != len(expected) {
+ t.Fatalf("expected %d results, got %d", len(expected), len(filtered))
+ }
+ for i, result := range filtered {
+ if result.Name != expected[i] {
+ t.Errorf("expected %s, got %s", expected[i], result.Name)
+ }
+ }
+ })
+
+ t.Run("Regex one or more +", func(t *testing.T) {
+ // Matches any name containing one or more 'e'
+ filtered, err := parseSearch(mockResults, "e+")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ expected := []string{"pidgey", "sandshrew", "charmander", "charmeleon", "squirtle", "caterpie", "weedle"}
+ if len(filtered) != len(expected) {
+ t.Fatalf("expected %d results, got %d", len(expected), len(filtered))
+ }
+ for i, result := range filtered {
+ if result.Name != expected[i] {
+ t.Errorf("expected %s, got %s", expected[i], result.Name)
+ }
+ }
+ })
+
+ t.Run("Regex optional ?", func(t *testing.T) {
+ filtered, err := parseSearch(mockResults, "^chu?arm")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ expected := []string{"charmander", "charmeleon"}
+ if len(filtered) != len(expected) {
+ t.Fatalf("expected %d results, got %d", len(expected), len(filtered))
+ }
+ for i, result := range filtered {
+ if result.Name != expected[i] {
+ t.Errorf("expected %s, got %s", expected[i], result.Name)
+ }
+ }
+ })
+
+ t.Run("Invalid regex returns error", func(t *testing.T) {
+ _, err := parseSearch(mockResults, "[invalid")
+ if err == nil {
+ t.Error("expected error for invalid regex, got nil")
+ }
+ })
}
func TestQuery(t *testing.T) {
@@ -68,8 +194,8 @@ func TestQuery(t *testing.T) {
return nil
}
- // Now call the query
- res, err := query("pokemon", "chu")
+ // Now call the query with regex pattern
+ res, err := query("pokemon", ".*chu.*")
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
diff --git a/go.mod b/go.mod
index a4c89cc..0876fed 100644
--- a/go.mod
+++ b/go.mod
@@ -47,6 +47,7 @@ require (
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect
+ github.com/schollz/closestmatch v2.1.0+incompatible // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/sys v0.38.0 // indirect
diff --git a/go.sum b/go.sum
index a31baba..78e742d 100644
--- a/go.sum
+++ b/go.sum
@@ -88,6 +88,8 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
+github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk=
+github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
diff --git a/nfpm.yaml b/nfpm.yaml
index e70c445..65a9d59 100644
--- a/nfpm.yaml
+++ b/nfpm.yaml
@@ -1,7 +1,7 @@
name: "poke-cli"
arch: "arm64"
platform: "linux"
-version: "v1.8.5"
+version: "v1.8.6"
section: "default"
version_schema: semver
maintainer: "Christian S"
diff --git a/testdata/cli_help.golden b/testdata/cli_help.golden
index 9596b16..3e7c9bb 100644
--- a/testdata/cli_help.golden
+++ b/testdata/cli_help.golden
@@ -1,32 +1,32 @@
-╭───────────────────────────────────────────────────────────────╮
-│Welcome! This tool displays data related to Pokémon! │
-│ │
-│ USAGE: │
-│ poke-cli [flag] │
-│ poke-cli [flag] │
-│ poke-cli [flag] │
-│ │
-│ FLAGS: │
-│ -h, --help Shows the help menu │
-│ -l, --latest Prints the latest version available │
-│ -v, --version Prints the current version │
-│ │
-│ COMMANDS: │
-│ ability Get details about an ability │
-│ berry Get details about a berry │
-│ card Get details about a TCG card │
-│ 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 │
-│ search Search for a resource │
-│ speed Calculate the speed of a Pokémon in battle │
-│ types Get details about a typing │
-│ │
-│ Use a hyphen when typing a name with a space. │
-│ example: poke-cli ability strong-jaw │
-│ example: poke-cli pokemon flutter-mane │
-│ │
-│ ↓ ctrl/cmd + click for docs/guides │
-│ ]8;;https://docs.poke-cli.com\docs.poke-cli.com]8;;\ │
-╰───────────────────────────────────────────────────────────────╯
+╭──────────────────────────────────────────────────────────────╮
+│Welcome! This tool displays data related to Pokémon! │
+│ │
+│ USAGE: │
+│ poke-cli [flag] │
+│ poke-cli [flag] │
+│ poke-cli [flag] │
+│ │
+│ FLAGS: │
+│ -h, --help Shows the help menu │
+│ -l, --latest Prints the latest version available │
+│ -v, --version Prints the current version │
+│ │
+│ COMMANDS: │
+│ ability Get details about an ability │
+│ berry Get details about a berry │
+│ card Get details about a TCG card │
+│ 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 │
+│ search Search for a resource │
+│ speed Calculate the speed of a Pokémon in battle│
+│ types Get details about a typing │
+│ │
+│ Use a hyphen when typing a name with a space. │
+│ example: poke-cli ability strong-jaw │
+│ example: poke-cli pokemon flutter-mane │
+│ │
+│ ↓ ctrl/cmd + click for docs/guides │
+│ ]8;;https://docs.poke-cli.com\docs.poke-cli.com]8;;\ │
+╰──────────────────────────────────────────────────────────────╯
diff --git a/testdata/cli_incorrect_command.golden b/testdata/cli_incorrect_command.golden
index 67df972..9481b63 100644
--- a/testdata/cli_incorrect_command.golden
+++ b/testdata/cli_incorrect_command.golden
@@ -1,18 +1,18 @@
-╭───────────────────────────────────────────────────────────────╮
-│✖ Error! │
-│ 'movesets' is not a valid command. │
-│ │
-│Commands: │
-│ ability Get details about an ability │
-│ berry Get details about a berry │
-│ card Get details about a TCG card │
-│ 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 │
-│ search Search for a resource │
-│ speed Calculate the speed of a Pokémon in battle │
-│ types Get details about a typing │
-│ │
-│Also run poke-cli -h for more info! │
-╰───────────────────────────────────────────────────────────────╯
+╭──────────────────────────────────────────────────────────────╮
+│✖ Error! │
+│ 'movesets' is not a valid command. │
+│ │
+│Commands: │
+│ ability Get details about an ability │
+│ berry Get details about a berry │
+│ card Get details about a TCG card │
+│ 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 │
+│ search Search for a resource │
+│ speed Calculate the speed of a Pokémon in battle│
+│ types Get details about a typing │
+│ │
+│Also run poke-cli -h for more info! │
+╰──────────────────────────────────────────────────────────────╯
diff --git a/testdata/main_latest_flag.golden b/testdata/main_latest_flag.golden
index 1b715f7..78b7cbb 100644
--- a/testdata/main_latest_flag.golden
+++ b/testdata/main_latest_flag.golden
@@ -2,6 +2,6 @@
┃ ┃
┃ Latest available release ┃
┃ on GitHub: ┃
-┃ • v1.8.4 ┃
+┃ • v1.8.5 ┃
┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛