diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 876491e6..36211ec2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ on: - main env: - VERSION_NUMBER: 'v1.9.0' + VERSION_NUMBER: 'v1.9.1' DOCKERHUB_REGISTRY_NAME: 'digitalghostdev/poke-cli' AWS_REGION: 'us-west-2' @@ -96,7 +96,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '1.24.6' + go-version: '1.25.8' - name: Build Go Binary env: diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index aa292cd3..5061b8c0 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -16,7 +16,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: 1.24 + go-version: 1.25 - name: Install dependencies run: | diff --git a/.github/workflows/go_lint.yml b/.github/workflows/go_lint.yml index 6c06f7f7..c9314a04 100644 --- a/.github/workflows/go_lint.yml +++ b/.github/workflows/go_lint.yml @@ -20,10 +20,10 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: 1.24 + go-version: 1.25 - name: Lint uses: golangci/golangci-lint-action@v7 with: - version: v2.0.1 + version: v2.11.4 skip-cache: true diff --git a/.github/workflows/go_test.yml b/.github/workflows/go_test.yml index efb93b38..8b72de58 100644 --- a/.github/workflows/go_test.yml +++ b/.github/workflows/go_test.yml @@ -16,7 +16,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: 1.24 + go-version: 1.25 - name: Install dependencies run: | diff --git a/.github/workflows/codspeed.yml b/.github/workflows/python_testing.yml similarity index 52% rename from .github/workflows/codspeed.yml rename to .github/workflows/python_testing.yml index 7c08c7d2..a188d6ae 100644 --- a/.github/workflows/codspeed.yml +++ b/.github/workflows/python_testing.yml @@ -1,21 +1,24 @@ -name: Codspeed Benchmarks +name: Python Tests + on: push: branches: - main paths: - 'card_data/**' + - 'web/**' pull_request: - types: [ opened, reopened, synchronize ] + types: [opened, reopened, synchronize] paths: - 'card_data/**' + - 'web/**' permissions: contents: read id-token: write jobs: - benchmarks: + card-data-benchmarks: runs-on: ubuntu-22.04 defaults: run: @@ -41,4 +44,28 @@ jobs: with: working-directory: card_data mode: simulation - run: uv run pytest pipelines/tests/ -v --codspeed \ No newline at end of file + run: uv run pytest pipelines/tests/ -v --codspeed + + streamlit-testing: + runs-on: ubuntu-22.04 + defaults: + run: + working-directory: web + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: '3.12' + + - name: Install uv + uses: astral-sh/setup-uv@v7 + + - name: Install dependencies + run: uv sync --dev + + - name: Run tests + run: uv run pytest app_test.py -v diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a3b2810d..d3f3c46d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: 1.24 + go-version: 1.25 - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 diff --git a/.goreleaser.yml b/.goreleaser.yml index e2d9e31c..fdaea092 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -14,7 +14,7 @@ builds: - windows - darwin ldflags: - - -s -w -X main.version=v1.9.0 + - -s -w -X main.version=v1.9.1 archives: - formats: [ 'zip' ] diff --git a/Dockerfile b/Dockerfile index 40c60e99..f3af9367 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # build 1 -FROM golang:1.24.12-alpine3.23 AS build +FROM golang:1.25.8-alpine3.23 AS build WORKDIR /app @@ -8,7 +8,7 @@ RUN go mod download COPY . . -RUN go build -ldflags "-X main.version=v1.9.0" -o poke-cli . +RUN go build -ldflags "-X main.version=v1.9.1" -o poke-cli . # build 2 FROM --platform=$BUILDPLATFORM alpine:3.23 diff --git a/README.md b/README.md index 885625f0..2d0fc9be 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ pokemon-logo

version-label - docker-image-size + docker-image-size ci-status-badge
@@ -99,15 +99,29 @@ 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.0 [subcommand] [flag] + docker run --rm -it digitalghostdev/poke-cli:v1.9.1 [subcommand] [flag] ``` * Enter the container and use its shell: ```bash - docker run --rm -it --name poke-cli --entrypoint /bin/sh digitalghostdev/poke-cli:v1.9.0 -c "cd /app && exec sh" + docker run --rm -it --name poke-cli --entrypoint /bin/sh digitalghostdev/poke-cli:v1.9.1 -c "cd /app && exec sh" # placed into the /app directory, run the program with './poke-cli' # example: ./poke-cli ability swift-swim ``` +> [!NOTE] +> 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.1 card +> +> # WezTerm, iTerm2, Ghostty, Konsole, Rio, Tabby +> docker run --rm -it -e TERM -e TERM_PROGRAM digitalghostdev/poke-cli:v1.9.1 card +> +> # Windows Terminal (Sixel) +> docker run --rm -it -e WT_SESSION digitalghostdev/poke-cli:v1.9.1 card +> ``` +> If your terminal is not listed above, image rendering is not supported inside Docker. + ### Binary 1. Head to the [releases](https://github.com/digitalghost-dev/poke-cli/releases) page of the project. diff --git a/card_data/pipelines/defs/extract/tcgcsv/extract_pricing.py b/card_data/pipelines/defs/extract/tcgcsv/extract_pricing.py index ba43e59f..5c9babe3 100644 --- a/card_data/pipelines/defs/extract/tcgcsv/extract_pricing.py +++ b/card_data/pipelines/defs/extract/tcgcsv/extract_pricing.py @@ -11,6 +11,7 @@ SET_PRODUCT_MATCHING = { # Mega Evolution + "me03": "24587", "me02.5": "24541", "me02": "24448", "me01": "24380", diff --git a/card_data/pipelines/defs/extract/tcgdex/extract_cards.py b/card_data/pipelines/defs/extract/tcgdex/extract_cards.py index 37f2f8b7..225b963f 100644 --- a/card_data/pipelines/defs/extract/tcgdex/extract_cards.py +++ b/card_data/pipelines/defs/extract/tcgdex/extract_cards.py @@ -10,7 +10,7 @@ @dg.asset(kinds={"API"}, name="extract_card_url_from_set_data") def extract_card_url_from_set() -> list: - urls = ["https://api.tcgdex.net/v2/en/sets/me02.5"] + urls = ["https://api.tcgdex.net/v2/en/sets/me03"] all_card_urls = [] diff --git a/card_data/pipelines/soda/checks_sets.yml b/card_data/pipelines/soda/checks_sets.yml index 42307555..54eb2b5d 100644 --- a/card_data/pipelines/soda/checks_sets.yml +++ b/card_data/pipelines/soda/checks_sets.yml @@ -1,6 +1,6 @@ checks for sets: # Row count validation - - row_count > 41 + - row_count > 50 # Schema validation checks - schema: diff --git a/card_data/pipelines/tests/extract_series_test.py b/card_data/pipelines/tests/extract_series_test.py index 1ce99587..0dbc2302 100644 --- a/card_data/pipelines/tests/extract_series_test.py +++ b/card_data/pipelines/tests/extract_series_test.py @@ -8,6 +8,7 @@ import responses from pipelines.defs.extract.tcgdex.extract_series import extract_series_data + @pytest.fixture def mock_api_response(): """Sample API response matching tcgdex format""" @@ -19,11 +20,10 @@ def mock_api_response(): {"id": "sm", "name": "Sun & Moon", "logo": None}, ] -@pytest.mark.benchmark + @responses.activate -def test_extract_series_data_success(mock_api_response): +def test_extract_series_data_success(benchmark, mock_api_response): """Test successful extraction and filtering""" - # Mock the API call responses.add( responses.GET, "https://api.tcgdex.net/v2/en/series", @@ -31,11 +31,10 @@ def test_extract_series_data_success(mock_api_response): status=200 ) - result = extract_series_data() + result = benchmark(extract_series_data) - # Assertions - assert isinstance(result, pl.DataFrame) # nosec - assert len(result) == 4 # nosec - assert set(result["id"].to_list()) == {"swsh", "sv", "me", "sm"} # nosec - assert "name" in result.columns # nosec - assert "logo" in result.columns # nosec \ No newline at end of file + assert isinstance(result, pl.DataFrame) # nosec + assert len(result) == 4 # nosec + assert set(result["id"].to_list()) == {"swsh", "sv", "me", "sm"} # nosec + assert "name" in result.columns # nosec + assert "logo" in result.columns # nosec diff --git a/card_data/pipelines/tests/extract_sets_test.py b/card_data/pipelines/tests/extract_sets_test.py index 521fdca1..e086ae47 100644 --- a/card_data/pipelines/tests/extract_sets_test.py +++ b/card_data/pipelines/tests/extract_sets_test.py @@ -82,11 +82,9 @@ def mock_api_response(): } -@pytest.mark.benchmark @responses.activate -def test_extract_sets_data_success(mock_api_response): +def test_extract_sets_data_success(benchmark, mock_api_response): """Test successful extraction of sets from multiple series""" - # Mock all API calls for url, response_data in mock_api_response.items(): responses.add( responses.GET, @@ -95,9 +93,8 @@ def test_extract_sets_data_success(mock_api_response): status=200, ) - result = extract_sets_data() + result = benchmark(extract_sets_data) - # Assertions assert isinstance(result, pl.DataFrame) # nosec assert len(result) == 6 # nosec (2 + 2 + 1 + 1 sets) assert set(result.columns) == { # nosec @@ -113,11 +110,9 @@ def test_extract_sets_data_success(mock_api_response): assert set(result["set_id"].to_list()) == {"me01", "me02", "sv01", "sv02", "swsh1", "sm1"} # nosec -@pytest.mark.benchmark @responses.activate -def test_extract_sets_data_empty_sets(mock_api_response): +def test_extract_sets_data_empty_sets(benchmark, mock_api_response): """Test extraction when a series has no sets""" - # Modify one response to have empty sets mock_api_response["https://api.tcgdex.net/v2/en/series/me"]["sets"] = [] for url, response_data in mock_api_response.items(): @@ -128,16 +123,15 @@ def test_extract_sets_data_empty_sets(mock_api_response): status=200, ) - result = extract_sets_data() + result = benchmark(extract_sets_data) assert isinstance(result, pl.DataFrame) # nosec assert len(result) == 4 # nosec (0 + 2 + 1 + 1 sets) assert "me" not in result["series_id"].to_list() # nosec -@pytest.mark.benchmark @responses.activate -def test_extract_sets_data_null_card_counts(): +def test_extract_sets_data_null_card_counts(benchmark): """Test extraction with null card counts""" mock_responses = { "https://api.tcgdex.net/v2/en/series/me": { @@ -178,7 +172,7 @@ def test_extract_sets_data_null_card_counts(): status=200, ) - result = extract_sets_data() + result = benchmark(extract_sets_data) assert isinstance(result, pl.DataFrame) # nosec assert len(result) == 1 # nosec diff --git a/card_data/pipelines/tests/json_retriever_test.py b/card_data/pipelines/tests/json_retriever_test.py index 316da8fc..f2697bdc 100644 --- a/card_data/pipelines/tests/json_retriever_test.py +++ b/card_data/pipelines/tests/json_retriever_test.py @@ -9,9 +9,8 @@ from pipelines.utils.json_retriever import fetch_json -@pytest.mark.benchmark @responses.activate -def test_fetch_json_success(): +def test_fetch_json_success(benchmark): """Test successful JSON retrieval.""" responses.add( responses.GET, @@ -20,16 +19,15 @@ def test_fetch_json_success(): status=200, ) - result = fetch_json("https://api.example.com/data") + result = benchmark(fetch_json, "https://api.example.com/data") assert isinstance(result, dict) # nosec assert result["id"] == 1 # nosec assert result["name"] == "Pikachu" # nosec -@pytest.mark.benchmark @responses.activate -def test_fetch_json_with_nested_data(): +def test_fetch_json_with_nested_data(benchmark): """Test retrieval of nested JSON structures.""" payload = { "results": [ @@ -45,16 +43,15 @@ def test_fetch_json_with_nested_data(): status=200, ) - result = fetch_json("https://api.example.com/products") + result = benchmark(fetch_json, "https://api.example.com/products") assert result["totalItems"] == 2 # nosec assert len(result["results"]) == 2 # nosec assert result["results"][0]["productId"] == 100 # nosec -@pytest.mark.benchmark @responses.activate -def test_fetch_json_http_404(): +def test_fetch_json_http_404(benchmark): """Test that a 404 response raises HTTPError.""" responses.add( responses.GET, @@ -63,13 +60,15 @@ def test_fetch_json_http_404(): status=404, ) - with pytest.raises(requests.exceptions.HTTPError): - fetch_json("https://api.example.com/missing") + def run(): + with pytest.raises(requests.exceptions.HTTPError): + fetch_json("https://api.example.com/missing") + + benchmark(run) -@pytest.mark.benchmark @responses.activate -def test_fetch_json_http_500(): +def test_fetch_json_http_500(benchmark): """Test that a 500 response raises HTTPError.""" responses.add( responses.GET, @@ -78,13 +77,15 @@ def test_fetch_json_http_500(): status=500, ) - with pytest.raises(requests.exceptions.HTTPError): - fetch_json("https://api.example.com/error") + def run(): + with pytest.raises(requests.exceptions.HTTPError): + fetch_json("https://api.example.com/error") + + benchmark(run) -@pytest.mark.benchmark @responses.activate -def test_fetch_json_connection_error(): +def test_fetch_json_connection_error(benchmark): """Test that a connection error raises ConnectionError.""" responses.add( responses.GET, @@ -92,13 +93,15 @@ def test_fetch_json_connection_error(): body=requests.exceptions.ConnectionError("Connection refused"), ) - with pytest.raises(requests.exceptions.ConnectionError): - fetch_json("https://api.example.com/down") + def run(): + with pytest.raises(requests.exceptions.ConnectionError): + fetch_json("https://api.example.com/down") + + benchmark(run) -@pytest.mark.benchmark @responses.activate -def test_fetch_json_timeout(): +def test_fetch_json_timeout(benchmark): """Test that a timeout raises an appropriate exception.""" responses.add( responses.GET, @@ -106,13 +109,15 @@ def test_fetch_json_timeout(): body=requests.exceptions.ReadTimeout("Read timed out"), ) - with pytest.raises(requests.exceptions.ReadTimeout): - fetch_json("https://api.example.com/slow") + def run(): + with pytest.raises(requests.exceptions.ReadTimeout): + fetch_json("https://api.example.com/slow") + + benchmark(run) -@pytest.mark.benchmark @responses.activate -def test_fetch_json_empty_object(): +def test_fetch_json_empty_object(benchmark): """Test retrieval of an empty JSON object.""" responses.add( responses.GET, @@ -121,14 +126,13 @@ def test_fetch_json_empty_object(): status=200, ) - result = fetch_json("https://api.example.com/empty") + result = benchmark(fetch_json, "https://api.example.com/empty") assert result == {} # nosec -@pytest.mark.benchmark @responses.activate -def test_fetch_json_invalid_json(): +def test_fetch_json_invalid_json(benchmark): """Test that an invalid JSON body raises a ValueError (JSONDecodeError).""" responses.add( responses.GET, @@ -138,5 +142,8 @@ def test_fetch_json_invalid_json(): content_type="application/json", ) - with pytest.raises(requests.exceptions.JSONDecodeError): - fetch_json("https://api.example.com/bad") + def run(): + with pytest.raises(requests.exceptions.JSONDecodeError): + fetch_json("https://api.example.com/bad") + + benchmark(run) diff --git a/card_data/pipelines/tests/secret_retriever_test.py b/card_data/pipelines/tests/secret_retriever_test.py index 4ba9f92d..b9664af5 100644 --- a/card_data/pipelines/tests/secret_retriever_test.py +++ b/card_data/pipelines/tests/secret_retriever_test.py @@ -8,7 +8,7 @@ from unittest.mock import patch, MagicMock from pipelines.utils.secret_retriever import fetch_secret, fetch_n8n_webhook_secret -@pytest.mark.benchmark + @patch("pipelines.utils.secret_retriever.SecretCache") @patch("pipelines.utils.secret_retriever.botocore.session.get_session") def test_fetch_secret_success(mock_get_session, mock_secret_cache_cls): @@ -25,7 +25,6 @@ def test_fetch_secret_success(mock_get_session, mock_secret_cache_cls): mock_cache_instance.get_secret_string.assert_called_once_with("supabase") -@pytest.mark.benchmark @patch("pipelines.utils.secret_retriever.SecretCache") @patch("pipelines.utils.secret_retriever.botocore.session.get_session") def test_fetch_secret_missing_key(mock_get_session, mock_secret_cache_cls): @@ -40,7 +39,6 @@ def test_fetch_secret_missing_key(mock_get_session, mock_secret_cache_cls): fetch_secret() -@pytest.mark.benchmark @patch("pipelines.utils.secret_retriever.SecretCache") @patch("pipelines.utils.secret_retriever.botocore.session.get_session") def test_fetch_secret_invalid_json(mock_get_session, mock_secret_cache_cls): @@ -53,7 +51,6 @@ def test_fetch_secret_invalid_json(mock_get_session, mock_secret_cache_cls): fetch_secret() -@pytest.mark.benchmark @patch("pipelines.utils.secret_retriever.SecretCache") @patch("pipelines.utils.secret_retriever.botocore.session.get_session") def test_fetch_secret_empty_json_object(mock_get_session, mock_secret_cache_cls): @@ -66,7 +63,6 @@ def test_fetch_secret_empty_json_object(mock_get_session, mock_secret_cache_cls) fetch_secret() -@pytest.mark.benchmark @patch("pipelines.utils.secret_retriever.SecretCache") @patch("pipelines.utils.secret_retriever.botocore.session.get_session") def test_fetch_secret_cache_raises(mock_get_session, mock_secret_cache_cls): @@ -86,7 +82,6 @@ def test_fetch_secret_cache_raises(mock_get_session, mock_secret_cache_cls): # --------------------------------------------------------------------------- -@pytest.mark.benchmark @patch("pipelines.utils.secret_retriever.SecretCache") @patch("pipelines.utils.secret_retriever.botocore.session.get_session") def test_fetch_n8n_webhook_secret_success(mock_get_session, mock_secret_cache_cls): @@ -103,7 +98,6 @@ def test_fetch_n8n_webhook_secret_success(mock_get_session, mock_secret_cache_cl mock_cache_instance.get_secret_string.assert_called_once_with("n8n_webhook") -@pytest.mark.benchmark @patch("pipelines.utils.secret_retriever.SecretCache") @patch("pipelines.utils.secret_retriever.botocore.session.get_session") def test_fetch_n8n_webhook_secret_missing_key(mock_get_session, mock_secret_cache_cls): @@ -118,7 +112,6 @@ def test_fetch_n8n_webhook_secret_missing_key(mock_get_session, mock_secret_cach fetch_n8n_webhook_secret() -@pytest.mark.benchmark @patch("pipelines.utils.secret_retriever.SecretCache") @patch("pipelines.utils.secret_retriever.botocore.session.get_session") def test_fetch_n8n_webhook_secret_invalid_json( @@ -133,7 +126,6 @@ def test_fetch_n8n_webhook_secret_invalid_json( fetch_n8n_webhook_secret() -@pytest.mark.benchmark @patch("pipelines.utils.secret_retriever.SecretCache") @patch("pipelines.utils.secret_retriever.botocore.session.get_session") def test_fetch_n8n_webhook_secret_empty_json_object( @@ -148,7 +140,6 @@ def test_fetch_n8n_webhook_secret_empty_json_object( fetch_n8n_webhook_secret() -@pytest.mark.benchmark @patch("pipelines.utils.secret_retriever.SecretCache") @patch("pipelines.utils.secret_retriever.botocore.session.get_session") def test_fetch_n8n_webhook_secret_cache_raises( diff --git a/cmd/card/card.go b/cmd/card/card.go index 6b9ef350..6881ed12 100644 --- a/cmd/card/card.go +++ b/cmd/card/card.go @@ -36,34 +36,32 @@ func CardCommand() (string, error) { return output.String(), err } - seriesModel := SeriesList() // Program 1: Series selection - finalModel, err := tea.NewProgram(seriesModel, tea.WithAltScreen()).Run() + finalModel, err := tea.NewProgram(SeriesList(), tea.WithAltScreen()).Run() if err != nil { return "", fmt.Errorf("error running series selection program: %w", err) } - result, ok := finalModel.(SeriesModel) + result, ok := finalModel.(seriesModel) if !ok { - return "", fmt.Errorf("unexpected model type from series selection: got %T, want SeriesModel", finalModel) + return "", fmt.Errorf("unexpected model type from series selection: got %T, want seriesModel", finalModel) } if result.SeriesID != "" { // Program 2: Sets selection - setsModel, err := SetsList(result.SeriesID) - + setsMdl, err := SetsList(result.SeriesID) if err != nil { return "", fmt.Errorf("error loading sets: %w", err) } - finalSetsModel, err := tea.NewProgram(setsModel, tea.WithAltScreen()).Run() + finalSetsModel, err := tea.NewProgram(setsMdl, tea.WithAltScreen()).Run() if err != nil { return "", fmt.Errorf("error running sets selection program: %w", err) } - setsResult, ok := finalSetsModel.(SetsModel) + setsResult, ok := finalSetsModel.(setsModel) if !ok { - return "", fmt.Errorf("unexpected model type from sets selection: got %T, want SetsModel", finalSetsModel) + return "", fmt.Errorf("unexpected model type from sets selection: got %T, want setsModel", finalSetsModel) } if setsResult.Quitting { @@ -72,34 +70,33 @@ func CardCommand() (string, error) { // Program 3: Cards display if setsResult.SetID != "" { - cardsModel, err := CardsList(setsResult.SetID) + cardsMdl, err := CardsList(setsResult.SetID) if err != nil { return "", fmt.Errorf("error loading cards: %w", err) } for { - finalCardsModel, err := tea.NewProgram(cardsModel, tea.WithAltScreen()).Run() + finalCardsModel, err := tea.NewProgram(cardsMdl, tea.WithAltScreen()).Run() if err != nil { return "", fmt.Errorf("error running cards program: %w", err) } - cardsResult, ok := finalCardsModel.(CardsModel) + cardsResult, ok := finalCardsModel.(cardsModel) if !ok { - return "", fmt.Errorf("unexpected model type from cards display: got %T, want CardsModel", finalCardsModel) + return "", fmt.Errorf("unexpected model type from cards display: got %T, want cardsModel", finalCardsModel) } if cardsResult.ViewImage { // Launch image viewer imageURL := cardsResult.ImageMap[cardsResult.SelectedOption] - imageModel := ImageRenderer(cardsResult.SelectedOption, imageURL) - _, err := tea.NewProgram(imageModel, tea.WithAltScreen()).Run() + _, err := tea.NewProgram(ImageRenderer(cardsResult.SelectedOption, imageURL), tea.WithAltScreen()).Run() if err != nil { fmt.Fprintf(os.Stderr, "Warning: image viewer error: %v\n", err) } // Re-launch cards with same state cardsResult.ViewImage = false - cardsModel = cardsResult + cardsMdl = cardsResult } else { break } diff --git a/cmd/card/cardlist.go b/cmd/card/cardlist.go index cead01d7..65c457a3 100644 --- a/cmd/card/cardlist.go +++ b/cmd/card/cardlist.go @@ -16,7 +16,7 @@ import ( var getCardData = connections.CallTCGData -type CardsModel struct { +type cardsModel struct { AllRows []table.Row Choice string Error error @@ -63,7 +63,7 @@ func cardTableStyles(selectedBg lipgloss.Color) table.Styles { return s } -func syncTableStylesForFocus(m *CardsModel) { +func syncTableStylesForFocus(m *cardsModel) { if m.Search.Focused() { m.TableStyles = cardTableStyles(inactiveTableSelectedBg) } else { @@ -126,14 +126,14 @@ func fetchCardsCmd(setID string) tea.Cmd { } } -func (m CardsModel) Init() tea.Cmd { +func (m cardsModel) Init() tea.Cmd { return tea.Batch( m.Spinner.Tick, fetchCardsCmd(m.SetID), ) } -func (m CardsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m cardsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var bubbleCmd tea.Cmd switch msg := msg.(type) { @@ -239,7 +239,7 @@ func (m CardsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, bubbleCmd } -func applyFilter(m *CardsModel) { +func applyFilter(m *cardsModel) { q := strings.TrimSpace(strings.ToLower(m.Search.Value())) if q == "" { m.Table.SetRows(m.AllRows) @@ -261,7 +261,7 @@ func applyFilter(m *CardsModel) { m.Table.SetCursor(0) } -func (m CardsModel) View() string { +func (m cardsModel) View() string { if m.Quitting { return "\n Quitting card search...\n\n" } @@ -319,12 +319,12 @@ type cardData struct { } // CardsList returns a minimal model - data fetching happens via Init() -func CardsList(setID string) (CardsModel, error) { +func CardsList(setID string) (cardsModel, error) { s := spinner.New() s.Spinner = spinner.Dot s.Style = styling.Yellow - return CardsModel{ + return cardsModel{ SetID: setID, Loading: true, Spinner: s, diff --git a/cmd/card/cardlist_test.go b/cmd/card/cardlist_test.go index 8563fbd5..9a982acd 100644 --- a/cmd/card/cardlist_test.go +++ b/cmd/card/cardlist_test.go @@ -33,7 +33,7 @@ func TestCardsModel_Update_EscKey(t *testing.T) { table.WithFocused(true), ) - model := CardsModel{ + model := cardsModel{ Table: tbl, Quitting: false, } @@ -41,7 +41,7 @@ func TestCardsModel_Update_EscKey(t *testing.T) { msg := tea.KeyMsg{Type: tea.KeyEsc} newModel, cmd := model.Update(msg) - resultModel := newModel.(CardsModel) + resultModel := newModel.(cardsModel) if !resultModel.Quitting { t.Error("Quitting should be set to true when ESC is pressed") @@ -65,7 +65,7 @@ func TestCardsModel_Update_CtrlC(t *testing.T) { table.WithFocused(true), ) - model := CardsModel{ + model := cardsModel{ Table: tbl, Quitting: false, } @@ -73,7 +73,7 @@ func TestCardsModel_Update_CtrlC(t *testing.T) { msg := tea.KeyMsg{Type: tea.KeyCtrlC} newModel, cmd := model.Update(msg) - resultModel := newModel.(CardsModel) + resultModel := newModel.(cardsModel) if !resultModel.Quitting { t.Error("Quitting should be set to true when Ctrl+C is pressed") @@ -100,7 +100,7 @@ func TestCardsModel_Update_TabTogglesSearchFocusAndTableSelectedBackground(t *te initialStyles := cardTableStyles(activeTableSelectedBg) tbl.SetStyles(initialStyles) - model := CardsModel{ + model := cardsModel{ Search: search, Table: tbl, TableStyles: initialStyles, @@ -108,7 +108,7 @@ func TestCardsModel_Update_TabTogglesSearchFocusAndTableSelectedBackground(t *te // Tab into the search bar. newModel, _ := model.Update(tea.KeyMsg{Type: tea.KeyTab}) - m1 := newModel.(CardsModel) + m1 := newModel.(cardsModel) if !m1.Search.Focused() { t.Fatal("expected search to be focused after tab") } @@ -122,7 +122,7 @@ func TestCardsModel_Update_TabTogglesSearchFocusAndTableSelectedBackground(t *te // Tab back to the table. newModel2, _ := m1.Update(tea.KeyMsg{Type: tea.KeyTab}) - m2 := newModel2.(CardsModel) + m2 := newModel2.(cardsModel) if m2.Search.Focused() { t.Fatal("expected search to be blurred after tabbing back") } @@ -148,7 +148,7 @@ func TestCardsModel_Update_ViewImageKey_QuestionMark(t *testing.T) { table.WithFocused(true), ) - model := CardsModel{ + model := cardsModel{ Table: tbl, ViewImage: false, } @@ -156,7 +156,7 @@ func TestCardsModel_Update_ViewImageKey_QuestionMark(t *testing.T) { msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'?'}} newModel, cmd := model.Update(msg) - resultModel := newModel.(CardsModel) + resultModel := newModel.(cardsModel) if !resultModel.ViewImage { t.Error("ViewImage should be set to true when '?' is pressed") @@ -180,7 +180,7 @@ func TestCardsModel_Update_ViewImageKey_DoesNotOverrideSearch(t *testing.T) { search := textinput.New() search.Focus() - model := CardsModel{ + model := cardsModel{ Search: search, Table: tbl, ViewImage: false, @@ -188,7 +188,7 @@ func TestCardsModel_Update_ViewImageKey_DoesNotOverrideSearch(t *testing.T) { msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'?'}} newModel, _ := model.Update(msg) - resultModel := newModel.(CardsModel) + resultModel := newModel.(cardsModel) if resultModel.ViewImage { t.Fatal("expected ViewImage to remain false when typing '?' in the search field") @@ -202,7 +202,7 @@ func TestCardsModel_Update_ViewImageKey_DoesNotOverrideSearch(t *testing.T) { } func TestCardsModel_View_Quitting(t *testing.T) { - model := CardsModel{ + model := cardsModel{ Quitting: true, } @@ -230,7 +230,7 @@ func TestCardsModel_View_PriceDisplay(t *testing.T) { "001/198 - Bulbasaur": "Price: $1.50", } - model := CardsModel{ + model := cardsModel{ Table: tbl, PriceMap: priceMap, Quitting: false, @@ -260,7 +260,7 @@ func TestCardsModel_View_MissingPrice(t *testing.T) { // Empty price map - simulates missing price data priceMap := map[string]string{} - model := CardsModel{ + model := cardsModel{ Table: tbl, PriceMap: priceMap, Quitting: false, @@ -320,7 +320,7 @@ func TestCardDataMsg_PopulatesModel(t *testing.T) { } newModel, _ := model.Update(msg) - resultModel := newModel.(CardsModel) + resultModel := newModel.(cardsModel) if resultModel.Loading { t.Error("Loading should be false after receiving data") @@ -360,7 +360,7 @@ func TestCardDataMsg_Error_StoresError(t *testing.T) { } newModel, cmd := model.Update(msg) - resultModel := newModel.(CardsModel) + resultModel := newModel.(cardsModel) if resultModel.Error == nil { t.Error("Error should be set when error received") @@ -392,7 +392,7 @@ func TestCardDataMsg_EmptyResult(t *testing.T) { } newModel, _ := model.Update(msg) - resultModel := newModel.(CardsModel) + resultModel := newModel.(cardsModel) if resultModel.Loading { t.Error("Loading should be false after receiving data") diff --git a/cmd/card/imageviewer.go b/cmd/card/imageviewer.go index 68e96ae5..88ba82f1 100644 --- a/cmd/card/imageviewer.go +++ b/cmd/card/imageviewer.go @@ -7,7 +7,7 @@ import ( "github.com/digitalghost-dev/poke-cli/styling" ) -type ImageModel struct { +type imageModel struct { CardName string ImageURL string Error error @@ -37,14 +37,14 @@ func fetchImageCmd(imageURL string) tea.Cmd { } } -func (m ImageModel) Init() tea.Cmd { +func (m imageModel) Init() tea.Cmd { return tea.Batch( m.Spinner.Tick, fetchImageCmd(m.ImageURL), ) } -func (m ImageModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m imageModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case imageReadyMsg: m.Loading = false @@ -72,7 +72,7 @@ func (m ImageModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } -func (m ImageModel) View() string { +func (m imageModel) View() string { if m.Loading { return lipgloss.NewStyle().Padding(2).Render( m.Spinner.View() + "Loading image for \n" + m.CardName, @@ -90,12 +90,12 @@ func (m ImageModel) View() string { return m.ImageData } -func ImageRenderer(cardName string, imageURL string) ImageModel { +func ImageRenderer(cardName string, imageURL string) imageModel { s := spinner.New() s.Spinner = spinner.Dot s.Style = styling.Yellow - return ImageModel{ + return imageModel{ CardName: cardName, ImageURL: imageURL, Loading: true, diff --git a/cmd/card/imageviewer_test.go b/cmd/card/imageviewer_test.go index 381014d9..e288c1b5 100644 --- a/cmd/card/imageviewer_test.go +++ b/cmd/card/imageviewer_test.go @@ -18,7 +18,7 @@ func TestImageModel_Init(t *testing.T) { } func TestImageModel_Update_EscKey(t *testing.T) { - model := ImageModel{ + model := imageModel{ CardName: "001/198 - Pineco", ImageURL: "test-sixel-data", } @@ -33,13 +33,13 @@ func TestImageModel_Update_EscKey(t *testing.T) { } // Model should be returned (even if quitting) - if _, ok := newModel.(ImageModel); !ok { + if _, ok := newModel.(imageModel); !ok { t.Error("Update should return ImageModel") } } func TestImageModel_Update_CtrlC(t *testing.T) { - model := ImageModel{ + model := imageModel{ CardName: "001/198 - Pineco", ImageURL: "test-sixel-data", } @@ -53,7 +53,7 @@ func TestImageModel_Update_CtrlC(t *testing.T) { } func TestImageModel_Update_DifferentKey(t *testing.T) { - model := ImageModel{ + model := imageModel{ CardName: "001/198 - Pineco", ImageURL: "test-sixel-data", } @@ -83,7 +83,7 @@ func TestImageModel_View_Loading(t *testing.T) { func TestImageModel_View_Loaded(t *testing.T) { expectedData := "test-sixel-data-123" - model := ImageModel{ + model := imageModel{ CardName: "001/198 - Pineco", ImageURL: "http://example.com/image.png", Loading: false, @@ -98,7 +98,7 @@ func TestImageModel_View_Loaded(t *testing.T) { } func TestImageModel_View_Empty(t *testing.T) { - model := ImageModel{ + model := imageModel{ CardName: "001/198 - Pineco", ImageURL: "", Loading: false, @@ -143,7 +143,7 @@ func TestImageModel_Update_ImageReady(t *testing.T) { t.Error("Update with imageReadyMsg should return nil command") } - updatedModel := newModel.(ImageModel) + updatedModel := newModel.(imageModel) if updatedModel.Loading { t.Error(`Update with imageReadyMsg should set Loading to false`) } @@ -173,7 +173,7 @@ func TestImageModel_Update_SpinnerTick(t *testing.T) { } // Model should still be ImageModel - if _, ok := newModel.(ImageModel); !ok { + if _, ok := newModel.(imageModel); !ok { t.Error("Update should return ImageModel") } } diff --git a/cmd/card/serieslist.go b/cmd/card/serieslist.go index ffe56fa5..9d7132e3 100644 --- a/cmd/card/serieslist.go +++ b/cmd/card/serieslist.go @@ -13,18 +13,18 @@ var seriesIDMap = map[string]string{ "Sun & Moon": "sm", } -type SeriesModel struct { +type seriesModel struct { List list.Model Choice string SeriesID string Quitting bool } -func (m SeriesModel) Init() tea.Cmd { +func (m seriesModel) Init() tea.Cmd { return nil } -func (m SeriesModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m seriesModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { @@ -50,7 +50,7 @@ func (m SeriesModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd } -func (m SeriesModel) View() string { +func (m seriesModel) View() string { if m.Quitting { return "\n Quitting card search...\n\n" } @@ -61,7 +61,7 @@ func (m SeriesModel) View() string { return "\n" + m.List.View() } -func SeriesList() SeriesModel { +func SeriesList() seriesModel { items := []list.Item{ styling.Item("Mega Evolution"), styling.Item("Scarlet & Violet"), @@ -80,5 +80,5 @@ func SeriesList() SeriesModel { l.Styles.PaginationStyle = styling.PaginationStyle l.Styles.HelpStyle = styling.HelpStyle - return SeriesModel{List: l} + return seriesModel{List: l} } diff --git a/cmd/card/serieslist_test.go b/cmd/card/serieslist_test.go index 4c1b71d2..ff295afe 100644 --- a/cmd/card/serieslist_test.go +++ b/cmd/card/serieslist_test.go @@ -17,7 +17,7 @@ func TestSeriesModelInit(t *testing.T) { styling.Item("Sword & Shield"), } l := list.New(items, styling.ItemDelegate{}, 20, 12) - model := SeriesModel{List: l} + model := seriesModel{List: l} cmd := model.Init() if cmd != nil { @@ -32,7 +32,7 @@ func TestSeriesModelQuit(t *testing.T) { styling.Item("Sword & Shield"), } l := list.New(items, styling.ItemDelegate{}, 20, 12) - model := SeriesModel{List: l} + model := seriesModel{List: l} testModel := teatest.NewTestModel(t, model, teatest.WithInitialTermSize(80, 24)) @@ -40,7 +40,7 @@ func TestSeriesModelQuit(t *testing.T) { testModel.Send(tea.KeyMsg{Type: tea.KeyCtrlC}) testModel.WaitFinished(t, teatest.WithFinalTimeout(300*time.Millisecond)) - final := testModel.FinalModel(t).(SeriesModel) + final := testModel.FinalModel(t).(seriesModel) if !final.Quitting { t.Errorf("Expected model to be quitting after ctrl+c") @@ -54,7 +54,7 @@ func TestSeriesModelEscQuit(t *testing.T) { styling.Item("Sword & Shield"), } l := list.New(items, styling.ItemDelegate{}, 20, 12) - model := SeriesModel{List: l} + model := seriesModel{List: l} testModel := teatest.NewTestModel(t, model, teatest.WithInitialTermSize(80, 24)) @@ -62,7 +62,7 @@ func TestSeriesModelEscQuit(t *testing.T) { testModel.Send(tea.KeyMsg{Type: tea.KeyEsc}) testModel.WaitFinished(t, teatest.WithFinalTimeout(300*time.Millisecond)) - final := testModel.FinalModel(t).(SeriesModel) + final := testModel.FinalModel(t).(seriesModel) if !final.Quitting { t.Errorf("Expected model to be quitting after esc") @@ -76,7 +76,7 @@ func TestSeriesModelSelection(t *testing.T) { styling.Item("Sword & Shield"), } l := list.New(items, styling.ItemDelegate{}, 20, 12) - model := SeriesModel{List: l} + model := seriesModel{List: l} testModel := teatest.NewTestModel(t, model, teatest.WithInitialTermSize(80, 24)) @@ -85,7 +85,7 @@ func TestSeriesModelSelection(t *testing.T) { testModel.Send(tea.KeyMsg{Type: tea.KeyEnter}) // Select it testModel.WaitFinished(t, teatest.WithFinalTimeout(300*time.Millisecond)) - final := testModel.FinalModel(t).(SeriesModel) + final := testModel.FinalModel(t).(seriesModel) if final.Choice == "" { t.Errorf("Expected a choice to be made, got empty string") @@ -102,11 +102,11 @@ func TestSeriesModelWindowResize(t *testing.T) { styling.Item("Sword & Shield"), } l := list.New(items, styling.ItemDelegate{}, 20, 12) - model := SeriesModel{List: l} + model := seriesModel{List: l} // Send window resize message updatedModel, _ := model.Update(tea.WindowSizeMsg{Width: 100, Height: 30}) - finalModel := updatedModel.(SeriesModel) + finalModel := updatedModel.(seriesModel) if finalModel.List.Width() != 100 { t.Errorf("Expected list width to be 100 after resize, got %d", finalModel.List.Width()) @@ -122,7 +122,7 @@ func TestSeriesModelView(t *testing.T) { l := list.New(items, styling.ItemDelegate{}, 20, 12) // Test normal view - model := SeriesModel{List: l} + model := seriesModel{List: l} view := model.View() if view == "" { t.Errorf("Expected non-empty view, got empty string") diff --git a/cmd/card/setslist.go b/cmd/card/setslist.go index c530449d..4c230800 100644 --- a/cmd/card/setslist.go +++ b/cmd/card/setslist.go @@ -13,7 +13,7 @@ import ( var getSetsData = connections.CallTCGData -type SetsModel struct { +type setsModel struct { Choice string Error error Loading bool @@ -64,14 +64,14 @@ func fetchSetsCmd(seriesID string) tea.Cmd { } } -func (m SetsModel) Init() tea.Cmd { +func (m setsModel) Init() tea.Cmd { return tea.Batch( m.Spinner.Tick, fetchSetsCmd(m.SeriesName), ) } -func (m SetsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m setsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { @@ -135,7 +135,7 @@ func (m SetsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } -func (m SetsModel) View() string { +func (m setsModel) View() string { if m.Error != nil { return styling.ApiErrorStyle.Render( "Error loading sets from Supabase:\n" + @@ -169,12 +169,12 @@ type setData struct { } // SetsList returns a minimal model - data fetching happens via Init() -func SetsList(seriesID string) (SetsModel, error) { +func SetsList(seriesID string) (setsModel, error) { s := spinner.New() s.Spinner = spinner.Dot s.Style = styling.Yellow - return SetsModel{ + return setsModel{ SeriesName: seriesID, Loading: true, Spinner: s, diff --git a/cmd/card/setslist_test.go b/cmd/card/setslist_test.go index 14c20c67..f823b0b8 100644 --- a/cmd/card/setslist_test.go +++ b/cmd/card/setslist_test.go @@ -26,7 +26,7 @@ func TestSetsModel_Update_EscKey(t *testing.T) { } l := list.New(items, styling.ItemDelegate{}, 20, 20) - model := SetsModel{ + model := setsModel{ List: l, SeriesName: "sv", Quitting: false, @@ -35,7 +35,7 @@ func TestSetsModel_Update_EscKey(t *testing.T) { msg := tea.KeyMsg{Type: tea.KeyEsc} newModel, cmd := model.Update(msg) - resultModel, ok := newModel.(SetsModel) + resultModel, ok := newModel.(setsModel) if !ok { t.Fatalf("expected SetsModel, got %T", newModel) } @@ -55,7 +55,7 @@ func TestSetsModel_Update_CtrlC(t *testing.T) { } l := list.New(items, styling.ItemDelegate{}, 20, 20) - model := SetsModel{ + model := setsModel{ List: l, SeriesName: "sv", Quitting: false, @@ -64,7 +64,7 @@ func TestSetsModel_Update_CtrlC(t *testing.T) { msg := tea.KeyMsg{Type: tea.KeyCtrlC} newModel, cmd := model.Update(msg) - resultModel, ok := newModel.(SetsModel) + resultModel, ok := newModel.(setsModel) if !ok { t.Fatalf("expected SetsModel, got %T", newModel) } @@ -84,7 +84,7 @@ func TestSetsModel_Update_WindowSizeMsg(t *testing.T) { } l := list.New(items, styling.ItemDelegate{}, 20, 20) - model := SetsModel{ + model := setsModel{ List: l, SeriesName: "sv", } @@ -92,7 +92,7 @@ func TestSetsModel_Update_WindowSizeMsg(t *testing.T) { msg := tea.WindowSizeMsg{Width: 100, Height: 50} newModel, cmd := model.Update(msg) - resultModel, ok := newModel.(SetsModel) + resultModel, ok := newModel.(setsModel) if !ok { t.Fatalf("expected SetsModel, got %T", newModel) } @@ -112,7 +112,7 @@ func TestSetsModel_View_Quitting(t *testing.T) { } l := list.New(items, styling.ItemDelegate{}, 20, 20) - model := SetsModel{ + model := setsModel{ List: l, Quitting: true, } @@ -130,7 +130,7 @@ func TestSetsModel_View_ChoiceMade(t *testing.T) { } l := list.New(items, styling.ItemDelegate{}, 20, 20) - model := SetsModel{ + model := setsModel{ List: l, Choice: "Scarlet & Violet", } @@ -148,7 +148,7 @@ func TestSetsModel_View_Normal(t *testing.T) { } l := list.New(items, styling.ItemDelegate{}, 20, 20) - model := SetsModel{ + model := setsModel{ List: l, Quitting: false, Choice: "", @@ -173,7 +173,7 @@ func TestSetsModel_Update_EnterKey(t *testing.T) { "Paldea Evolved": "sv02", } - model := SetsModel{ + model := setsModel{ List: l, SetsIDMap: setsIDMap, } @@ -225,7 +225,7 @@ func TestSetsDataMsg_PopulatesModel(t *testing.T) { } newModel, _ := model.Update(msg) - resultModel := newModel.(SetsModel) + resultModel := newModel.(setsModel) if resultModel.Loading { t.Error("Loading should be false after receiving data") @@ -248,7 +248,7 @@ func TestSetsDataMsg_Error_StoresError(t *testing.T) { } newModel, cmd := model.Update(msg) - resultModel := newModel.(SetsModel) + resultModel := newModel.(setsModel) if resultModel.Error == nil { t.Error("Error should be set when error received") @@ -278,7 +278,7 @@ func TestSetsDataMsg_EmptyResult(t *testing.T) { } newModel, _ := model.Update(msg) - resultModel := newModel.(SetsModel) + resultModel := newModel.(setsModel) if resultModel.Loading { t.Error("Loading should be false after receiving data") diff --git a/cmd/search/model_input.go b/cmd/search/model_input.go index c25ed6e8..a73f0f4d 100644 --- a/cmd/search/model_input.go +++ b/cmd/search/model_input.go @@ -11,7 +11,7 @@ import ( ) // UpdateInput handles text input updates. -func UpdateInput(msg tea.Msg, m Model) (tea.Model, tea.Cmd) { +func UpdateInput(msg tea.Msg, m model) (tea.Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { case tea.KeyMsg: @@ -63,7 +63,7 @@ func UpdateInput(msg tea.Msg, m Model) (tea.Model, tea.Cmd) { } // RenderInput renders the input view. -func RenderInput(m Model) (string, string) { +func RenderInput(m model) (string, string) { var msg string var endpoint string diff --git a/cmd/search/model_input_test.go b/cmd/search/model_input_test.go index 3d71a251..011e36f3 100644 --- a/cmd/search/model_input_test.go +++ b/cmd/search/model_input_test.go @@ -11,7 +11,7 @@ func TestUpdateInput(t *testing.T) { ti := textinput.New() ti.SetValue("mewtwo") - m := Model{ + m := model{ ShowResults: true, TextInput: ti, } @@ -19,7 +19,7 @@ func TestUpdateInput(t *testing.T) { msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'b'}} mUpdated, _ := UpdateInput(msg, m) - updated := mUpdated.(Model) + updated := mUpdated.(model) if updated.ShowResults { t.Errorf("expected ShowResults to be false after pressing 'b'") diff --git a/cmd/search/model_selection.go b/cmd/search/model_selection.go index 02f5d573..05611949 100644 --- a/cmd/search/model_selection.go +++ b/cmd/search/model_selection.go @@ -9,7 +9,7 @@ import ( ) // UpdateSelection handles navigation in the selection menu. -func UpdateSelection(msg tea.Msg, m Model) (tea.Model, tea.Cmd) { +func UpdateSelection(msg tea.Msg, m model) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { @@ -33,7 +33,7 @@ func UpdateSelection(msg tea.Msg, m Model) (tea.Model, tea.Cmd) { } // RenderSelection renders the selection menu. -func RenderSelection(m Model) string { +func RenderSelection(m model) string { c := m.Choice greeting := styling.StyleItalic.Render("Search for a resource and return a matching selection table") choices := fmt.Sprintf( diff --git a/cmd/search/model_selection_test.go b/cmd/search/model_selection_test.go index 05786d88..4751bdfb 100644 --- a/cmd/search/model_selection_test.go +++ b/cmd/search/model_selection_test.go @@ -9,8 +9,8 @@ import ( ) func TestSelection(t *testing.T) { - model := initialModel() - testModel := teatest.NewTestModel(t, model, teatest.WithInitialTermSize(500, 600)) + m := initialModel() + testModel := teatest.NewTestModel(t, m, teatest.WithInitialTermSize(500, 600)) testModel.Send(tea.KeyMsg{Type: tea.KeyDown}) testModel.Send(tea.KeyMsg{Type: tea.KeyUp}) @@ -19,7 +19,7 @@ func TestSelection(t *testing.T) { testModel.Send(tea.KeyMsg{Type: tea.KeyCtrlC}) testModel.WaitFinished(t, teatest.WithFinalTimeout(300*time.Millisecond)) - final := testModel.FinalModel(t).(Model) + final := testModel.FinalModel(t).(model) if !final.Chosen { t.Errorf("Expected model to be in Chosen state after pressing enter") @@ -36,8 +36,8 @@ func TestSelection(t *testing.T) { } func TestChoiceClamping(t *testing.T) { - model := initialModel() - testModel := teatest.NewTestModel(t, model) + m := initialModel() + testModel := teatest.NewTestModel(t, m) // Move down twice, this should attempt to exceed max Choice testModel.Send(tea.KeyMsg{Type: tea.KeyDown}) // 0 → 1 @@ -53,7 +53,7 @@ func TestChoiceClamping(t *testing.T) { testModel.Send(tea.KeyMsg{Type: tea.KeyCtrlC}) testModel.WaitFinished(t) - final := testModel.FinalModel(t).(Model) + final := testModel.FinalModel(t).(model) if final.Choice != 0 && final.Choice != 1 { t.Errorf("Choice should be clamped between 0 and 1, got %d", final.Choice) diff --git a/cmd/search/search.go b/cmd/search/search.go index 6951a7f2..66199f65 100644 --- a/cmd/search/search.go +++ b/cmd/search/search.go @@ -43,8 +43,8 @@ func SearchCommand() (string, error) { return output.String(), nil } -// Model structure -type Model struct { +// model structure +type model struct { Choice int Chosen bool Quitting bool @@ -54,24 +54,24 @@ type Model struct { WarningMessage string } -func initialModel() Model { +func initialModel() model { ti := textinput.New() ti.Placeholder = "type name..." ti.CharLimit = 20 ti.Width = 20 - return Model{ + return model{ TextInput: ti, } } // Init initializes the program. -func (m Model) Init() tea.Cmd { +func (m model) Init() tea.Cmd { return nil } // Update handles keypresses and updates the state. -func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { @@ -88,7 +88,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // View renders the correct UI screen. -func (m Model) View() string { +func (m model) View() string { if m.Quitting { return "\n Quitting search...\n\n" } diff --git a/cmd/search/search_test.go b/cmd/search/search_test.go index 4da0682e..e0e8db52 100644 --- a/cmd/search/search_test.go +++ b/cmd/search/search_test.go @@ -57,19 +57,19 @@ func TestSearchCommand(t *testing.T) { } func TestModelInit(t *testing.T) { - m := Model{} + m := model{} cmd := m.Init() assert.Nil(t, cmd, "Init() should return nil") } func TestModelQuit(t *testing.T) { - m := Model{} + m := model{} // Simulate pressing Esc msg := tea.KeyMsg{Type: tea.KeyEsc} newModel, cmd := m.Update(msg) - assert.True(t, newModel.(Model).Quitting, "Model should be set to quitting") + assert.True(t, newModel.(model).Quitting, "Model should be set to quitting") if cmd != nil { assert.Equal(t, cmd(), tea.Quit(), "Update() should return tea.Quit command") @@ -90,13 +90,13 @@ func TestSearchCommandValidationError(t *testing.T) { } func TestModelViewQuitting(t *testing.T) { - m := Model{Quitting: true} + m := model{Quitting: true} view := m.View() assert.Contains(t, view, "Quitting search", "View should show quitting message") } func TestModelViewShowResults(t *testing.T) { - m := Model{ + m := model{ ShowResults: true, SearchResults: "Test Results", } @@ -106,7 +106,7 @@ func TestModelViewShowResults(t *testing.T) { } func TestModelViewNotChosen(t *testing.T) { - m := Model{Chosen: false} + m := model{Chosen: false} view := m.View() // View calls RenderSelection when not chosen assert.Contains(t, view, "Search for a resource", "View should show selection prompt") diff --git a/docs/Dockerfile b/docs/Dockerfile index f62b43a4..70ae6d5d 100644 --- a/docs/Dockerfile +++ b/docs/Dockerfile @@ -17,19 +17,13 @@ USER docsuser RUN mkdocs build -# --- Serve with lightweight HTTP server --- -FROM python:3.12-slim +# --- Serve with nginx --- +FROM nginx:alpine -# Create non-root user for runtime -RUN groupadd -r -g 10001 docsuser && \ - useradd -r -u 10001 -g docsuser -m -s /sbin/nologin docsuser - -WORKDIR /site +COPY --from=builder /build/site /usr/share/nginx/html -COPY --from=builder --chown=docsuser:docsuser /build/site /site - -USER docsuser +COPY docs/nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 8080 -CMD ["python3", "-m", "http.server", "8080"] \ No newline at end of file +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/docs/Infrastructure_Guide/aws.md b/docs/Infrastructure_Guide/aws.md index bf4b8b0a..794f061d 100644 --- a/docs/Infrastructure_Guide/aws.md +++ b/docs/Infrastructure_Guide/aws.md @@ -22,6 +22,7 @@ local deployment and moves it into the cloud. * [VPC](#vpc) * [RDS](#rds) * [EC2](#ec2) +* [S3](#s3) * [Elastic IPs](#elastic-ips) * [EventBridge](#eventbridge) * [Secrets Manager](#secrets-manager) @@ -195,9 +196,78 @@ AWS EC2 (Elastic Compute Cloud) is a cloud service that provides resizable virtu --- +## S3 +AWS S3 is a cloud storage service that lets you store and retrieve any number of files (called "objects") from anywhere on the internet. + +This project uses s3 to store demo GIFs of the CLI and assets for the web app. + +1. Visit the [S3 console](https://console.aws.amazon.com/s3). +2. Click on **Create Bucket**. +3. Select **general purpose** for the bucket type, then **global namespace** for the bucket namespace. +4. Give the bucket a name. +5. Under the **Object Ownership** section, choose **ACLs disabled (recommended)**. +6. Choose to block all public access. (CloudFront will be set up as a CDN) +7. Disable bucket versioning. +8. Optionally, add tags for organizational purposes. +9. Leave **Default Encryption** section with default options. +10. Click **Create Bucket**. + +### CloudFront +AWS CloudFront is a content delivery network (CDN) that sits in front of your S3 files, serving content from servers closest to the user for faster load times, while also providing DDoS protection. + +This project uses CloudFront to serve the static assets of the web app. + +1. Visit the [CloudFront console](https://console.aws.amazon.com/cloudfront/home). +2. Click on create distribution + +* Step 1 // Choose a Plan + 1. Choose the free plan, click next +* Step 2 // Get Started + 1. Provide a name for the distribution + 2. Project doesn’t have domains on Route 53 + 3. Add tags for organization, click next +* Step 3 // Specify Origin + 1. Origin type is Amazon S3 + 2. Click **Browse S3** and choose the correct bucket + 3. Keep recommended settings, click next +* Step 4 // Enable Security + 1. Enable **Use monitor mode** +* Step 5 // Review and Create + 1. Review the configuration and then click **Create Distribution** when ready +* Step 6 // Update S3 Bucket Policy + 1. Visit the S3 [homepage](https://us-east-1.console.aws.amazon.com/s3/home) and select the bucket + 2. Under the **Permissions** tab, in the **Block public access** section, ensure that block _all_ public access is on. + 3. In the **Bucket Policy** section, click on **Edit**, paste in the following policy (fill missing values): + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowCloudFrontServicePrincipal", + "Effect": "Allow", + "Principal": { + "Service": "cloudfront.amazonaws.com" + }, + "Action": "s3:GetObject", + "Resource": "arn:aws:s3:::/*", + "Condition": { + "ArnLike": { + "AWS:SourceArn": "arn:aws:cloudfront:::distribution/" + } + } + } + ] + } + ``` + 4. Save the policy changes. + +--- + ## Elastic IPs An Elastic IP is a static public IPv4 address in AWS that can be assigned to an EC2 instance. +A static IP address is used to ensure the virtual machine can reliably connect to the PostgreSQL database on every startup. + 1. Visit the [EC2 console](https://console.aws.amazon.com/ec2). 2. On the left, under **Network & Security**, click on **Elastic IPs**. 3. In the upper-right, click on **Allocate Elastic IP Address**. diff --git a/docs/assets/tcg.gif b/docs/assets/tcg.gif new file mode 100644 index 00000000..5def3412 Binary files /dev/null and b/docs/assets/tcg.gif differ diff --git a/docs/commands.md b/docs/commands.md index d52e3bf8..f2478e84 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -4,7 +4,6 @@ **Available Flags** -* `--help | -h` * `--latest | -l` * `--version | -v` @@ -16,13 +15,12 @@ the generation in which it first appeared, and a list of Pokémon that possess i **Available Flags** -* `--help | -h` * `--pokemon | -p` Example: ```console -$ poke-cli ability solar-power -$ poke-cli ability solar-power --pokemon # list Pokémon that posses the ability +poke-cli ability solar-power +poke-cli ability solar-power --pokemon # list Pokémon that posses the ability ``` Output: @@ -36,7 +34,7 @@ Output: Example: ```console -$ poke-cli item poke-ball +poke-cli item poke-ball ``` Output: @@ -51,7 +49,7 @@ and the move's effect. Example: ```console -$ poke-cli move dazzling-gleam +poke-cli move dazzling-gleam ``` Output: @@ -65,7 +63,7 @@ Output: Example: ```console -$ poke-cli natures +poke-cli natures ``` Output: @@ -79,7 +77,6 @@ Output: **Available Flags** -* `-h | --help` * `-a | --abilities` * `-d | --defense` * `-i=xx | --image=xx` @@ -94,7 +91,7 @@ Output: Example: ```console -$ poke-cli pokemon rockruff --abilities --moves +poke-cli pokemon rockruff --abilities --moves ``` Output: @@ -103,7 +100,7 @@ Output: Example: ```console -$ poke-cli pokemon gastrodon --defense +poke-cli pokemon gastrodon --defense ``` Output: @@ -113,7 +110,7 @@ Output: Example: ```console # choose between three sizes: 'sm', 'md', 'lg' -$ poke-cli pokemon tyranitar --image=sm +poke-cli pokemon tyranitar --image=sm ``` Output: @@ -122,7 +119,7 @@ Output: Example: ```console -$ poke-cli pokemon cacturne --stats +poke-cli pokemon cacturne --stats ``` Output: @@ -136,7 +133,7 @@ Output: Example: ```console -$ poke-cli search +poke-cli search ``` Output: @@ -150,7 +147,7 @@ Output: Example: ```console -$ poke-cli speed +poke-cli speed ``` Output: @@ -158,12 +155,30 @@ Output: --- +## `tcg` +* Retrieve details about all competitive TCG tournaments for the current season. + +**Available Flags** + +* `--web | -w` - Open the tournament's website in the default browser. + +Example: +```console +poke-cli tcg +``` + +Output: + +![tcg_command](assets/tcg.gif) + +--- + ## `types` * Retrieve details about a specific type and a damage relation table. Example: ```console -$ poke-cli types +poke-cli types ``` Output: diff --git a/docs/index.md b/docs/index.md index e56ea268..dfa629a0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -13,5 +13,12 @@ including CI/CD pipelines with GitHub Actions and data workflows using Dagster a View the [guides](Infrastructure_Guide/index.md) section for more details. ## Demo -Here is a quick demo of the CLI/TUI tool in action. -![demo_gif](https://poke-cli-s3-bucket.s3.us-west-2.amazonaws.com/demo-v1.6.0.gif) \ No newline at end of file +Here are some demos of the CLI/TUI tool in action. + +### Video Game Data + +![demo-vg](https://dc8hq8aq7pr04.cloudfront.net/demo-v1.6.0.gif) + +### Trading Card Game Data + +![demo-tcg](https://dc8hq8aq7pr04.cloudfront.net/poke-cli-card-v1.8.8.gif) \ No newline at end of file diff --git a/docs/nginx.conf b/docs/nginx.conf new file mode 100644 index 00000000..df13beb0 --- /dev/null +++ b/docs/nginx.conf @@ -0,0 +1,15 @@ +server { + listen 8080; + root /usr/share/nginx/html; + index index.html; + + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self';" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + location / { + try_files $uri $uri/ $uri.html =404; + } +} diff --git a/go.mod b/go.mod index 956b178c..b74cb0d1 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/digitalghost-dev/poke-cli -go 1.24.12 +go 1.25.8 require ( github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 diff --git a/nfpm.yaml b/nfpm.yaml index 8291cec7..aa968e2a 100644 --- a/nfpm.yaml +++ b/nfpm.yaml @@ -1,7 +1,7 @@ name: "poke-cli" arch: "arm64" platform: "linux" -version: "v1.9.0" +version: "v1.9.1" section: "default" version_schema: semver maintainer: "Christian S" diff --git a/testdata/main_latest_flag.golden b/testdata/main_latest_flag.golden index 0d62e917..4a9faff1 100644 --- a/testdata/main_latest_flag.golden +++ b/testdata/main_latest_flag.golden @@ -2,6 +2,6 @@ ┃ ┃ ┃ Latest available release ┃ ┃ on GitHub: ┃ -┃ • v1.8.11 ┃ +┃ • v1.9.0 ┃ ┃ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛