Skip to content

Add new get-matches command for direct tournament match access - #3

Merged
mgranderath merged 2 commits into
mainfrom
feature/get-matches-command
Feb 1, 2026
Merged

Add new get-matches command for direct tournament match access#3
mgranderath merged 2 commits into
mainfrom
feature/get-matches-command

Conversation

@mgranderath

@mgranderath mgranderath commented Feb 1, 2026

Copy link
Copy Markdown
Owner

Summary

This PR adds a new get-matches command that fetches matches directly from the Blast API /matches endpoint, providing a simpler alternative to the existing get-brackets command which extracts matches from bracket structures.

Changes

New Features

  • get-matches command: Fetches all matches for a tournament directly from the API
    • Supports same filters as get-brackets: --completed-only, --live-only, --upcoming-only, --team, --match-type
    • Supports multiple output formats: table (default), json, yaml
    • Team filter searches both team name and shorthand

New Files Created

  • internal/api/blast/match_models.go - API types for matches endpoint
  • internal/mapper/match.go - Maps API response to domain models with status inference
  • internal/output/matches_formatter.go - Formatter interface and registry
  • internal/output/matches_table.go - Table formatter for matches
  • internal/output/matches_json.go - JSON formatter
  • internal/output/matches_yaml.go - YAML formatter
  • internal/cmd/get_matches.go - Command implementation

Test Coverage

All new functionality includes comprehensive tests:

  • internal/mapper/match_test.go (93.4% coverage)
    • Status inference from map timestamps
    • Time parsing with optional fields
  • internal/output/*_test.go (76.1% coverage)
    • Table, JSON, YAML formatting
    • Formatter registry
  • internal/cmd/get_matches_test.go (81.6% coverage)
    • Filter logic (status, team, match type)
    • HTTP mocking with various responses

Modified Files

  • internal/cmd/root.go - Added get-matches command to CLI

Usage Examples

# Get all matches for a tournament
rlcs-cli get-matches rlcs-kick-off-lan-2026

# Filter by team (matches name or shorthand)
rlcs-cli get-matches rlcs-kick-off-lan-2026 --team=kc

# Show only completed matches
rlcs-cli get-matches rlcs-kick-off-lan-2026 --completed-only

# Output as JSON
rlcs-cli get-matches rlcs-kick-off-lan-2026 --output=json

# Filter by match type
rlcs-cli get-matches rlcs-kick-off-lan-2026 --match-type=BO5

Implementation Notes

  • Status Inference: Since the matches API doesn't provide explicit isLive/isCompleted booleans, status is inferred from map timestamps:

    • Completed: All started maps have ended
    • Live: Some maps started but not all ended
    • Upcoming: No maps have started
  • Reusable Components: The implementation reuses existing domain models (domain.Match, domain.MatchTeam, domain.MatchMap) and follows the same patterns as get-brackets.

Testing

All tests pass with good coverage:

go test ./...
ok  	github.com/mgranderath/rlcs-cli/internal/cmd    	0.480s	coverage: 81.6% of statements
ok  	github.com/mgranderath/rlcs-cli/internal/mapper 	0.335s	coverage: 93.4% of statements
ok  	github.com/mgranderath/rlcs-cli/internal/output 	0.489s	coverage: 76.1% of statements

Summary by CodeRabbit

  • New Features

    • Added a new get-matches CLI to fetch tournament matches with status (completed, live, upcoming), team and type filters, and output in table, JSON, or YAML.
    • Added data models and mapping to convert API match responses into internal match representations.
    • Added table, JSON, and YAML formatters for match output.
  • Tests

    • Extensive unit and integration tests covering retrieval, mapping, filtering, formatter behavior, and error cases.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Feb 1, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a new "get-matches" CLI command that fetches matches from an external API, maps API responses to domain models, supports multiple in-memory filters (status, team, match type), and outputs results in table, JSON, or YAML formats.

Changes

Cohort / File(s) Summary
API Response Models
internal/api/blast/match_models.go
New exported types modeling match JSON (MatchResponse, MatchResponseTeam, MatchResponseMap, MatchResponseMetadata, Stage).
CLI Command & Tests
internal/cmd/get_matches.go, internal/cmd/get_matches_test.go
New GetMatchesCmd with Run(), HTTP fetch and error handling, mutual-exclusion validation for status flags, mapping, filtering (matchesFilters, applyFilters), output selection; comprehensive unit tests.
Command Registration
internal/cmd/root.go
Registers GetMatches GetMatchesCmd in CLI root struct.
Domain Mapping & Tests
internal/mapper/match.go, internal/mapper/match_test.go
Mapper that converts blast.MatchResponse → domain.Match, parses timestamps, maps teams/maps, infers IsLive/IsCompleted; tests for multiple scenarios and edge cases.
Output Formatter Infra & Tests
internal/output/matches_formatter.go, internal/output/matches_formatter_test.go
Introduces MatchesFormatter interface, MatchesFormat type (table/json/yaml), formatter registry and GetMatchesFormatter with validation tests.
JSON Formatter & Tests
internal/output/matches_json.go, internal/output/matches_json_test.go
MatchesJSONFormatter formats indented JSON; tests for single/multiple/empty/nested/nil cases.
Table Formatter & Tests
internal/output/matches_table.go, internal/output/matches_table_test.go
MatchesTableFormatter renders ASCII table, truncates long fields, derives status (LIVE/Completed/Upcoming); tests for formatting, truncation, and status.
YAML Formatter & Tests
internal/output/matches_yaml.go, internal/output/matches_yaml_test.go
MatchesYAMLFormatter encodes matches as 2-space YAML; tests verify fields and nesting.

Sequence Diagram(s)

sequenceDiagram
    participant User as User/CLI
    participant Cmd as GetMatchesCmd
    participant HTTP as HTTP Client
    participant API as External API
    participant Mapper as Mapper
    participant Filter as Filter Logic
    participant Formatter as Output Formatter
    participant Stdout as stdout

    User->>Cmd: Run(ctx)
    Cmd->>HTTP: GET /matches (with timeout)
    HTTP->>API: request
    API-->>HTTP: MatchResponse[]
    HTTP-->>Cmd: response
    Cmd->>Mapper: ToDomainMatchesFromResponse(response)
    Mapper-->>Cmd: []domain.Match
    Cmd->>Filter: applyFilters(matches)
    Filter-->>Cmd: filtered matches
    Cmd->>Formatter: Format(stdout, filtered matches)
    Formatter->>Stdout: write output
    Stdout-->>User: display
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 A rabbit scurries, code in paw,

Fetching matches, mapping awe,
Filters hop and format gleam,
Table, JSON, YAML—what a dream!
Hoppity hops, the CLI beams ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main change: adding a new 'get-matches' CLI command for accessing tournament matches directly from the Blast API, which is the primary focus of this changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/get-matches-command

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @mgranderath, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the CLI's functionality by introducing a dedicated get-matches command. This command provides a more direct and efficient way for users to retrieve tournament match information, bypassing the need to process complex bracket data. It offers robust filtering capabilities and flexible output options, making it easier to query and consume match data. The implementation includes intelligent logic to determine match status, ensuring accurate and up-to-date information is presented to the user.

Highlights

  • New get-matches Command: A new CLI command has been introduced to directly fetch tournament match data from the Blast API, offering a streamlined alternative to extracting matches from bracket structures.
  • Flexible Filtering Options: The new command supports various filters including match status (--completed-only, --live-only, --upcoming-only), team name/shorthand, and match type (--match-type).
  • Multiple Output Formats: Users can now choose between table (default), json, and yaml output formats for match data, enhancing data usability.
  • Intelligent Match Status Inference: The system infers match status (Live, Completed, Upcoming) based on the timestamps of individual maps within a match, as the API does not provide explicit status flags.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new get-matches command, which is a great addition for directly accessing match data. The implementation is well-structured, with clear separation between the command, API models, mappers, and output formatters. The test coverage is also comprehensive.

I've identified a significant bug in the match status inference logic that could misclassify completed matches. I've also included a couple of suggestions to improve code modularity and reduce duplication. Overall, this is a solid contribution.

Comment thread internal/mapper/match.go
Comment on lines +112 to +141
func inferMatchStatus(maps []blast.MatchResponseMap) (isCompleted bool, isLive bool) {
if len(maps) == 0 {
return false, false
}

hasStarted := false
allEnded := true

for _, m := range maps {
if m.StartedAt != "" {
hasStarted = true
if m.EndedAt == "" {
allEnded = false
}
} else {
// Map hasn't started, so match isn't complete yet
allEnded = false
}
}

if !hasStarted {
return false, false // Upcoming
}

if allEnded {
return true, false // Completed
}

return false, true // Live
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current implementation of inferMatchStatus incorrectly assumes that all possible maps in a series must be played for a match to be considered complete. This will cause matches that finish early (e.g., a 3-0 in a Best-of-5) to be misclassified as 'Live'. The logic should only consider maps that have actually started to determine completion status. Additionally, it would be beneficial to add a test case for this scenario to prevent future regressions.

func inferMatchStatus(maps []blast.MatchResponseMap) (isCompleted bool, isLive bool) {
	if len(maps) == 0 {
		return false, false
	}

	hasStarted := false
	allStartedHaveEnded := true

	for _, m := range maps {
		if m.StartedAt != "" {
			hasStarted = true
			if m.EndedAt == "" {
				allStartedHaveEnded = false
			}
		}
	}

	if !hasStarted {
		return false, false // Upcoming
	}

	if allStartedHaveEnded {
		return true, false // Completed
	}

	return false, true // Live
}

Comment on lines +61 to +134
func (g *GetMatchesCmd) Run(ctx *Context) error {
// Validate conflicting filters
filterCount := 0
if g.CompletedOnly {
filterCount++
}
if g.LiveOnly {
filterCount++
}
if g.UpcomingOnly {
filterCount++
}
if filterCount > 1 {
return fmt.Errorf("cannot use multiple status filters together (completed-only, live-only, upcoming-only are mutually exclusive)")
}

url := fmt.Sprintf("https://api.blast.tv/v2/games/rl/tournaments/%s/matches", g.TournamentID)

client := &http.Client{
Timeout: 10 * time.Second,
}

req, err := http.NewRequest("GET", url, nil)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}

req.Header.Set("Accept", "application/json")

resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("failed to make request: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode == http.StatusNotFound {
return fmt.Errorf("tournament not found: %s", g.TournamentID)
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}

body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %w", err)
}

var apiMatches []blast.MatchResponse
if err := json.Unmarshal(body, &apiMatches); err != nil {
return fmt.Errorf("failed to parse JSON: %w", err)
}

// Map API response to domain model
matches, err := mapper.ToDomainMatchesFromResponse(apiMatches)
if err != nil {
return fmt.Errorf("failed to map matches: %w", err)
}

// Apply filters
matches = g.applyFilters(matches)

// Get the appropriate formatter
formatter, err := output.GetMatchesFormatter(g.Output)
if err != nil {
return fmt.Errorf("failed to get formatter: %w", err)
}

// Output using the selected formatter
if err := formatter.Format(os.Stdout, matches); err != nil {
return fmt.Errorf("failed to format output: %w", err)
}

return nil
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The Run function is responsible for a wide range of tasks, including filter validation, HTTP requests, response parsing, data mapping, filtering, and output formatting. To improve modularity and make the code easier to read and test, consider extracting the data fetching and parsing logic (lines 77-111) into a separate unexported function, such as fetchAndParseMatches(). This would allow Run to act as a higher-level coordinator, improving separation of concerns.

Comment thread internal/mapper/match.go
Comment on lines +77 to +94
// Handle empty time strings for fields that may not be set
var actualStart time.Time
if api.StartedAt != "" {
parsedTime, err := time.Parse(timeFormat, api.StartedAt)
if err != nil {
return domain.MatchMap{}, fmt.Errorf("failed to parse started at time: %w", err)
}
actualStart = parsedTime
}

var matchEnded time.Time
if api.EndedAt != "" {
parsedTime, err := time.Parse(timeFormat, api.EndedAt)
if err != nil {
return domain.MatchMap{}, fmt.Errorf("failed to parse ended at time: %w", err)
}
matchEnded = parsedTime
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The code for parsing optional timestamp strings (StartedAt and EndedAt) is duplicated. To improve maintainability and reduce redundancy, this logic can be extracted into a helper function.

For example, you could add a helper function like this and use it as shown in the suggestion:

func parseOptionalTime(timeStr, fieldName string) (time.Time, error) {
	if timeStr == "" {
		return time.Time{}, nil
	}
	t, err := time.Parse(timeFormat, timeStr)
	if err != nil {
		return time.Time{}, fmt.Errorf("failed to parse %s time: %w", fieldName, err)
	}
	return t, nil
}
Suggested change
// Handle empty time strings for fields that may not be set
var actualStart time.Time
if api.StartedAt != "" {
parsedTime, err := time.Parse(timeFormat, api.StartedAt)
if err != nil {
return domain.MatchMap{}, fmt.Errorf("failed to parse started at time: %w", err)
}
actualStart = parsedTime
}
var matchEnded time.Time
if api.EndedAt != "" {
parsedTime, err := time.Parse(timeFormat, api.EndedAt)
if err != nil {
return domain.MatchMap{}, fmt.Errorf("failed to parse ended at time: %w", err)
}
matchEnded = parsedTime
}
// Handle empty time strings for fields that may not be set
actualStart, err := parseOptionalTime(api.StartedAt, "started at")
if err != nil {
return domain.MatchMap{}, err
}
matchEnded, err := parseOptionalTime(api.EndedAt, "ended at")
if err != nil {
return domain.MatchMap{}, err
}

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@internal/output/matches_yaml_test.go`:
- Around line 120-129: The test TestMatchesYAMLFormatter_FormatEmpty currently
accepts either "[]" or "" for an empty slice which can hide write failures;
update the expectation to require the YAML-encoded empty array only. Change the
assertion in TestMatchesYAMLFormatter_FormatEmpty to call formatter.Format(&buf,
[]domain.Match{}) and then assert that strings.TrimSpace(buf.String()) == "[]",
removing the allowance for an empty string so any failure to write is detected;
reference the MatchesYAMLFormatter.Format method and the
TestMatchesYAMLFormatter_FormatEmpty test where the buffer contents are checked.
- Around line 108-115: The test currently loops over lines (variable lines from
output) but never fails if the 2-space indentation is missing; add a boolean
flag (e.g., foundTwoSpaceIndent) initialized false, set it true inside the loop
when the condition if strings.HasPrefix(line, "  ") && !strings.HasPrefix(line,
"    ") is met (then break), and after the loop assert the flag is true (e.g.,
using require.True/ assert.True with the test T) so the test actually validates
that a 2-space-indented line was present.

In `@internal/output/matches_yaml.go`:
- Around line 13-17: In MatchesYAMLFormatter.Format, change the deferred call to
yaml.Encoder.Close() so its error is captured and returned: don't just defer
encoder.Close() silently — call encoder.Encode(matches), capture its error, then
call encoder.Close() and if Close returns a non-nil error prefer/aggregate it
(or return it if Encode succeeded) so buffered write errors aren't swallowed;
update logic around yaml.NewEncoder, encoder.SetIndent, encoder.Encode and
encoder.Close to ensure both Encode and Close errors are handled and a non-nil
error is returned to the caller.
🧹 Nitpick comments (2)
internal/mapper/match_test.go (1)

368-442: Consider adding a test case for invalid EndedAt time.

The TestToDomainMatchMapFromResponse covers invalid ScheduledAt and invalid StartedAt, but doesn't test the error path for an invalid EndedAt value. This would improve coverage of the parsing logic at lines 88-94 in match.go.

📝 Suggested test case to add
{
    name: "invalid ended time",
    api: blast.MatchResponseMap{
        ID:          "map-5",
        Name:        "Invalid",
        ScheduledAt: "2026-01-15T12:00:00.000Z",
        StartedAt:   "2026-01-15T12:05:00.000Z",
        EndedAt:     "invalid",
    },
    expectError: true,
},
internal/cmd/get_matches.go (1)

77-101: Consider using request context for cancellation support.

The HTTP request is created without a context, which means it cannot be cancelled if the CLI receives a termination signal during a long-running request. While the 10-second timeout provides a safety net, using http.NewRequestWithContext with a cancellable context would improve responsiveness to user interrupts.

♻️ Suggested improvement
+import "context"

 func (g *GetMatchesCmd) Run(ctx *Context) error {
     // ...validation...

     url := fmt.Sprintf("https://api.blast.tv/v2/games/rl/tournaments/%s/matches", g.TournamentID)

     client := &http.Client{
         Timeout: 10 * time.Second,
     }

-    req, err := http.NewRequest("GET", url, nil)
+    reqCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+    defer cancel()
+
+    req, err := http.NewRequestWithContext(reqCtx, "GET", url, nil)
     if err != nil {
         return fmt.Errorf("failed to create request: %w", err)
     }

Comment thread internal/output/matches_yaml_test.go
Comment thread internal/output/matches_yaml_test.go Outdated
Comment thread internal/output/matches_yaml.go Outdated
- Fix critical bug in inferMatchStatus: unplayed maps no longer prevent
  completed matches from being correctly classified
- Fix YAML encoder to properly handle Close() errors
- Fix YAML tests: stricter empty check and proper indentation validation
- Add test cases for invalid EndedAt time and early-ending matches (BO7 4-0)
@mgranderath
mgranderath merged commit 0a33250 into main Feb 1, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant