cricinfo-cli-go 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +63 -0
- package/CONTRIBUTORS.md +75 -0
- package/LICENSE +21 -0
- package/Makefile +131 -0
- package/README.md +130 -0
- package/bin/cricinfo.js +44 -0
- package/cmd/cricinfo/main.go +15 -0
- package/go.mod +10 -0
- package/go.sum +10 -0
- package/internal/app/app.go +11 -0
- package/internal/app/app_test.go +122 -0
- package/internal/buildinfo/buildinfo.go +16 -0
- package/internal/cli/analysis.go +262 -0
- package/internal/cli/analysis_test.go +175 -0
- package/internal/cli/competitions.go +154 -0
- package/internal/cli/competitions_test.go +165 -0
- package/internal/cli/leagues.go +297 -0
- package/internal/cli/leagues_test.go +194 -0
- package/internal/cli/matches.go +403 -0
- package/internal/cli/matches_test.go +413 -0
- package/internal/cli/players.go +263 -0
- package/internal/cli/players_test.go +384 -0
- package/internal/cli/root.go +141 -0
- package/internal/cli/search.go +119 -0
- package/internal/cli/teams.go +214 -0
- package/internal/cli/teams_test.go +192 -0
- package/internal/cricinfo/analysis.go +1401 -0
- package/internal/cricinfo/analysis_phase15_test.go +267 -0
- package/internal/cricinfo/client.go +471 -0
- package/internal/cricinfo/client_test.go +280 -0
- package/internal/cricinfo/cmd/fixture-refresh/main.go +145 -0
- package/internal/cricinfo/competitions.go +405 -0
- package/internal/cricinfo/competitions_phase13_test.go +234 -0
- package/internal/cricinfo/coverage_ledger.go +122 -0
- package/internal/cricinfo/coverage_ledger_test.go +253 -0
- package/internal/cricinfo/decode.go +115 -0
- package/internal/cricinfo/decode_test.go +100 -0
- package/internal/cricinfo/entity_index.go +618 -0
- package/internal/cricinfo/entity_index_test.go +175 -0
- package/internal/cricinfo/fixture_matrix.go +243 -0
- package/internal/cricinfo/fixture_matrix_test.go +49 -0
- package/internal/cricinfo/fixtures_test.go +264 -0
- package/internal/cricinfo/historical_hydration.go +1641 -0
- package/internal/cricinfo/historical_phase14_test.go +542 -0
- package/internal/cricinfo/leagues.go +1210 -0
- package/internal/cricinfo/leagues_phase12_test.go +324 -0
- package/internal/cricinfo/live_leagues_test.go +169 -0
- package/internal/cricinfo/live_matches_test.go +203 -0
- package/internal/cricinfo/live_matrix_test.go +118 -0
- package/internal/cricinfo/live_players_test.go +122 -0
- package/internal/cricinfo/live_search_test.go +86 -0
- package/internal/cricinfo/live_smoke_test.go +213 -0
- package/internal/cricinfo/live_teams_test.go +104 -0
- package/internal/cricinfo/matches.go +1508 -0
- package/internal/cricinfo/matches_phase7_test.go +207 -0
- package/internal/cricinfo/matches_phase9_test.go +253 -0
- package/internal/cricinfo/normalize_entities.go +1727 -0
- package/internal/cricinfo/normalize_leagues.go +346 -0
- package/internal/cricinfo/players.go +1332 -0
- package/internal/cricinfo/players_phase10_test.go +174 -0
- package/internal/cricinfo/players_phase11_test.go +373 -0
- package/internal/cricinfo/render_contract.go +1088 -0
- package/internal/cricinfo/render_phase4_test.go +633 -0
- package/internal/cricinfo/renderer.go +1689 -0
- package/internal/cricinfo/resolver.go +813 -0
- package/internal/cricinfo/resolver_test.go +244 -0
- package/internal/cricinfo/teams.go +603 -0
- package/internal/cricinfo/teams_phase8_test.go +231 -0
- package/internal/cricinfo/testdata/fixtures/README.md +43 -0
- package/internal/cricinfo/testdata/fixtures/aux-competition-metadata/broadcasts.json +11 -0
- package/internal/cricinfo/testdata/fixtures/aux-competition-metadata/officials.json +150 -0
- package/internal/cricinfo/testdata/fixtures/details-plays/detail-110.json +157 -0
- package/internal/cricinfo/testdata/fixtures/details-plays/detail-52545007.json +145 -0
- package/internal/cricinfo/testdata/fixtures/details-plays/detail-52559021.json +143 -0
- package/internal/cricinfo/testdata/fixtures/details-plays/plays.json +15 -0
- package/internal/cricinfo/testdata/fixtures/endpoint-matrix.tsv +19 -0
- package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/fow-1.json +12 -0
- package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/fow.json +42 -0
- package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/innings-1-2.json +38 -0
- package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/partnership-1.json +31 -0
- package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/partnerships.json +42 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/calendar-offdays.json +20 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/calendar-ondays.json +21 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/calendar.json +14 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-2025.json +13 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-group-1.json +13 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-groups.json +11 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-type-1.json +13 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-types.json +11 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/seasons.json +30 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/standings-item-1.json +72 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/standings-root.json +3 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/standings.json +15 -0
- package/internal/cricinfo/testdata/fixtures/matches-competitions/competition.json +460 -0
- package/internal/cricinfo/testdata/fixtures/matches-competitions/event-1529474.json +86 -0
- package/internal/cricinfo/testdata/fixtures/matches-competitions/matchcards-1527966.json +368 -0
- package/internal/cricinfo/testdata/fixtures/matches-competitions/situation-1529474.json +10 -0
- package/internal/cricinfo/testdata/fixtures/players/athlete-1361257-statistics.json +126 -0
- package/internal/cricinfo/testdata/fixtures/players/athlete-1361257.json +113 -0
- package/internal/cricinfo/testdata/fixtures/players/roster-1361257-linescores-1-1-statistics-0.json +208 -0
- package/internal/cricinfo/testdata/fixtures/players/roster-1361257-linescores-1-2-statistics-0.json +252 -0
- package/internal/cricinfo/testdata/fixtures/players/roster-1361257-linescores.json +74 -0
- package/internal/cricinfo/testdata/fixtures/players/roster-1361257-statistics-0.json +1008 -0
- package/internal/cricinfo/testdata/fixtures/root-discovery/events.json +72 -0
- package/internal/cricinfo/testdata/fixtures/root-discovery/root.json +28 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/competitor-789643.json +40 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/leaders-789643.json +353 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/records-789643.json +91 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/roster-1147772-object.json +231 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/roster-1147772.json +235 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/roster-789643.json +322 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/scores-789643.json +19 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/statistics-789643.json +629 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/team-789643-athletes.json +7 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/team-789643.json +67 -0
- package/internal/cricinfo/testdata/golden/match-empty.golden +1 -0
- package/internal/cricinfo/testdata/golden/match-list.golden +2 -0
- package/internal/cricinfo/testdata/golden/match-partial.golden +3 -0
- package/internal/cricinfo/types.go +54 -0
- package/package.json +51 -0
- package/scripts/postinstall.js +153 -0
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
package cricinfo
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"strings"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// TeamServiceConfig configures team discovery and match-scoped team commands.
|
|
10
|
+
type TeamServiceConfig struct {
|
|
11
|
+
Client *Client
|
|
12
|
+
Resolver *Resolver
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// TeamLookupOptions controls resolver-backed team lookup behavior.
|
|
16
|
+
type TeamLookupOptions struct {
|
|
17
|
+
LeagueID string
|
|
18
|
+
MatchQuery string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// TeamService implements domain-level team and competitor commands.
|
|
22
|
+
type TeamService struct {
|
|
23
|
+
client *Client
|
|
24
|
+
resolver *Resolver
|
|
25
|
+
ownsResolver bool
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// NewTeamService builds a team service using default client/resolver when omitted.
|
|
29
|
+
func NewTeamService(cfg TeamServiceConfig) (*TeamService, error) {
|
|
30
|
+
client := cfg.Client
|
|
31
|
+
if client == nil {
|
|
32
|
+
var err error
|
|
33
|
+
client, err = NewClient(Config{})
|
|
34
|
+
if err != nil {
|
|
35
|
+
return nil, err
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
resolver := cfg.Resolver
|
|
40
|
+
ownsResolver := false
|
|
41
|
+
if resolver == nil {
|
|
42
|
+
var err error
|
|
43
|
+
resolver, err = NewResolver(ResolverConfig{Client: client})
|
|
44
|
+
if err != nil {
|
|
45
|
+
return nil, err
|
|
46
|
+
}
|
|
47
|
+
ownsResolver = true
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return &TeamService{
|
|
51
|
+
client: client,
|
|
52
|
+
resolver: resolver,
|
|
53
|
+
ownsResolver: ownsResolver,
|
|
54
|
+
}, nil
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Close persists resolver cache when owned by this service.
|
|
58
|
+
func (s *TeamService) Close() error {
|
|
59
|
+
if !s.ownsResolver || s.resolver == nil {
|
|
60
|
+
return nil
|
|
61
|
+
}
|
|
62
|
+
return s.resolver.Close()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Show resolves and returns one team summary, merging global team identity with match-scoped competitor fields when available.
|
|
66
|
+
func (s *TeamService) Show(ctx context.Context, teamQuery string, opts TeamLookupOptions) (NormalizedResult, error) {
|
|
67
|
+
lookup, passthrough := s.resolveTeamLookup(ctx, teamQuery, opts)
|
|
68
|
+
if passthrough != nil {
|
|
69
|
+
passthrough.Kind = EntityTeam
|
|
70
|
+
return *passthrough, nil
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
result := NewDataResult(EntityTeam, lookup.team)
|
|
74
|
+
if len(lookup.warnings) > 0 {
|
|
75
|
+
result = NewPartialResult(EntityTeam, lookup.team, lookup.warnings...)
|
|
76
|
+
}
|
|
77
|
+
if lookup.teamResolved != nil {
|
|
78
|
+
result.RequestedRef = lookup.teamResolved.RequestedRef
|
|
79
|
+
result.CanonicalRef = lookup.teamResolved.CanonicalRef
|
|
80
|
+
}
|
|
81
|
+
return result, nil
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Roster resolves and returns a team roster. Without --match it uses global team athletes; with --match it uses competitor roster.
|
|
85
|
+
func (s *TeamService) Roster(ctx context.Context, teamQuery string, opts TeamLookupOptions) (NormalizedResult, error) {
|
|
86
|
+
lookup, passthrough := s.resolveTeamLookup(ctx, teamQuery, opts)
|
|
87
|
+
if passthrough != nil {
|
|
88
|
+
passthrough.Kind = EntityTeamRoster
|
|
89
|
+
return *passthrough, nil
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
warnings := append([]string{}, lookup.warnings...)
|
|
93
|
+
useMatchScope := strings.TrimSpace(opts.MatchQuery) != "" && lookup.match != nil
|
|
94
|
+
|
|
95
|
+
rosterRef := ""
|
|
96
|
+
scope := TeamScopeGlobal
|
|
97
|
+
if useMatchScope {
|
|
98
|
+
rosterRef = nonEmpty(strings.TrimSpace(lookup.team.RosterRef), competitorSubresourceRef(*lookup.match, lookup.team.ID, "roster"))
|
|
99
|
+
scope = TeamScopeMatch
|
|
100
|
+
} else {
|
|
101
|
+
rosterRef = nonEmpty(extensionRef(lookup.team.Extensions, "athletes"), "/teams/"+strings.TrimSpace(lookup.team.ID)+"/athletes")
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if strings.TrimSpace(rosterRef) == "" {
|
|
105
|
+
result := NormalizedResult{
|
|
106
|
+
Kind: EntityTeamRoster,
|
|
107
|
+
Status: ResultStatusEmpty,
|
|
108
|
+
Message: fmt.Sprintf("roster route unavailable for team %q", lookup.team.ID),
|
|
109
|
+
}
|
|
110
|
+
return result, nil
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
resolved, err := s.client.ResolveRefChain(ctx, rosterRef)
|
|
114
|
+
if err != nil {
|
|
115
|
+
return NewTransportErrorResult(EntityTeamRoster, rosterRef, err), nil
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
var entries []TeamRosterEntry
|
|
119
|
+
if scope == TeamScopeMatch {
|
|
120
|
+
entries, err = NormalizeTeamRosterEntries(resolved.Body, lookup.team, scope, lookup.match.ID)
|
|
121
|
+
} else {
|
|
122
|
+
entries, err = NormalizeTeamAthletePage(resolved.Body, lookup.team)
|
|
123
|
+
}
|
|
124
|
+
if err != nil {
|
|
125
|
+
return NormalizedResult{}, fmt.Errorf("normalize team roster %q: %w", resolved.CanonicalRef, err)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
s.enrichRosterEntries(entries)
|
|
129
|
+
|
|
130
|
+
items := make([]any, 0, len(entries))
|
|
131
|
+
for _, entry := range entries {
|
|
132
|
+
items = append(items, entry)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
result := NewListResult(EntityTeamRoster, items)
|
|
136
|
+
if len(warnings) > 0 {
|
|
137
|
+
result = NewPartialListResult(EntityTeamRoster, items, warnings...)
|
|
138
|
+
}
|
|
139
|
+
result.RequestedRef = resolved.RequestedRef
|
|
140
|
+
result.CanonicalRef = resolved.CanonicalRef
|
|
141
|
+
return result, nil
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Scores resolves and returns one match-scoped team score response.
|
|
145
|
+
func (s *TeamService) Scores(ctx context.Context, teamQuery string, opts TeamLookupOptions) (NormalizedResult, error) {
|
|
146
|
+
lookup, passthrough := s.resolveTeamLookup(ctx, teamQuery, opts)
|
|
147
|
+
if passthrough != nil {
|
|
148
|
+
passthrough.Kind = EntityTeamScore
|
|
149
|
+
return *passthrough, nil
|
|
150
|
+
}
|
|
151
|
+
if lookup.match == nil {
|
|
152
|
+
return matchScopeRequiredResult(EntityTeamScore), nil
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
scoreRef := nonEmpty(strings.TrimSpace(lookup.team.ScoreRef), competitorSubresourceRef(*lookup.match, lookup.team.ID, "scores"))
|
|
156
|
+
if scoreRef == "" {
|
|
157
|
+
return NormalizedResult{Kind: EntityTeamScore, Status: ResultStatusEmpty, Message: fmt.Sprintf("score route unavailable for team %q", lookup.team.ID)}, nil
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
resolved, err := s.client.ResolveRefChain(ctx, scoreRef)
|
|
161
|
+
if err != nil {
|
|
162
|
+
return NewTransportErrorResult(EntityTeamScore, scoreRef, err), nil
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
score, err := NormalizeTeamScore(resolved.Body, lookup.team, TeamScopeMatch, lookup.match.ID)
|
|
166
|
+
if err != nil {
|
|
167
|
+
return NormalizedResult{}, fmt.Errorf("normalize team score %q: %w", resolved.CanonicalRef, err)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
result := NewDataResult(EntityTeamScore, score)
|
|
171
|
+
if len(lookup.warnings) > 0 {
|
|
172
|
+
result = NewPartialResult(EntityTeamScore, score, lookup.warnings...)
|
|
173
|
+
}
|
|
174
|
+
result.RequestedRef = resolved.RequestedRef
|
|
175
|
+
result.CanonicalRef = resolved.CanonicalRef
|
|
176
|
+
return result, nil
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Leaders resolves and returns one match-scoped team leaders payload.
|
|
180
|
+
func (s *TeamService) Leaders(ctx context.Context, teamQuery string, opts TeamLookupOptions) (NormalizedResult, error) {
|
|
181
|
+
lookup, passthrough := s.resolveTeamLookup(ctx, teamQuery, opts)
|
|
182
|
+
if passthrough != nil {
|
|
183
|
+
passthrough.Kind = EntityTeamLeaders
|
|
184
|
+
return *passthrough, nil
|
|
185
|
+
}
|
|
186
|
+
if lookup.match == nil {
|
|
187
|
+
return matchScopeRequiredResult(EntityTeamLeaders), nil
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
leadersRef := nonEmpty(strings.TrimSpace(lookup.team.LeadersRef), competitorSubresourceRef(*lookup.match, lookup.team.ID, "leaders"))
|
|
191
|
+
if leadersRef == "" {
|
|
192
|
+
return NormalizedResult{Kind: EntityTeamLeaders, Status: ResultStatusEmpty, Message: fmt.Sprintf("leaders route unavailable for team %q", lookup.team.ID)}, nil
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
resolved, err := s.client.ResolveRefChain(ctx, leadersRef)
|
|
196
|
+
if err != nil {
|
|
197
|
+
return NewTransportErrorResult(EntityTeamLeaders, leadersRef, err), nil
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
leaders, err := NormalizeTeamLeaders(resolved.Body, lookup.team, TeamScopeMatch, lookup.match.ID)
|
|
201
|
+
if err != nil {
|
|
202
|
+
return NormalizedResult{}, fmt.Errorf("normalize team leaders %q: %w", resolved.CanonicalRef, err)
|
|
203
|
+
}
|
|
204
|
+
leaders.TeamName = nonEmpty(leaders.TeamName, lookup.team.ShortName, lookup.team.Name)
|
|
205
|
+
leaders.Name = nonEmpty(leaders.Name, leaders.TeamName, "Leaders")
|
|
206
|
+
s.enrichTeamLeaders(ctx, leaders)
|
|
207
|
+
|
|
208
|
+
result := NewDataResult(EntityTeamLeaders, leaders)
|
|
209
|
+
if len(lookup.warnings) > 0 {
|
|
210
|
+
result = NewPartialResult(EntityTeamLeaders, leaders, lookup.warnings...)
|
|
211
|
+
}
|
|
212
|
+
result.RequestedRef = resolved.RequestedRef
|
|
213
|
+
result.CanonicalRef = resolved.CanonicalRef
|
|
214
|
+
return result, nil
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Statistics resolves and returns match-scoped team statistics categories.
|
|
218
|
+
func (s *TeamService) Statistics(ctx context.Context, teamQuery string, opts TeamLookupOptions) (NormalizedResult, error) {
|
|
219
|
+
lookup, passthrough := s.resolveTeamLookup(ctx, teamQuery, opts)
|
|
220
|
+
if passthrough != nil {
|
|
221
|
+
passthrough.Kind = EntityTeamStatistics
|
|
222
|
+
return *passthrough, nil
|
|
223
|
+
}
|
|
224
|
+
if lookup.match == nil {
|
|
225
|
+
return matchScopeRequiredResult(EntityTeamStatistics), nil
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
statisticsRef := nonEmpty(strings.TrimSpace(lookup.team.StatisticsRef), competitorSubresourceRef(*lookup.match, lookup.team.ID, "statistics"))
|
|
229
|
+
if statisticsRef == "" {
|
|
230
|
+
return NormalizedResult{Kind: EntityTeamStatistics, Status: ResultStatusEmpty, Message: fmt.Sprintf("statistics route unavailable for team %q", lookup.team.ID)}, nil
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
resolved, err := s.client.ResolveRefChain(ctx, statisticsRef)
|
|
234
|
+
if err != nil {
|
|
235
|
+
return NewTransportErrorResult(EntityTeamStatistics, statisticsRef, err), nil
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
categories, err := NormalizeStatCategories(resolved.Body)
|
|
239
|
+
if err != nil {
|
|
240
|
+
return NormalizedResult{}, fmt.Errorf("normalize team statistics %q: %w", resolved.CanonicalRef, err)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
items := make([]any, 0, len(categories))
|
|
244
|
+
for _, category := range categories {
|
|
245
|
+
items = append(items, category)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
result := NewListResult(EntityTeamStatistics, items)
|
|
249
|
+
if len(lookup.warnings) > 0 {
|
|
250
|
+
result = NewPartialListResult(EntityTeamStatistics, items, lookup.warnings...)
|
|
251
|
+
}
|
|
252
|
+
result.RequestedRef = resolved.RequestedRef
|
|
253
|
+
result.CanonicalRef = resolved.CanonicalRef
|
|
254
|
+
return result, nil
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Records resolves and returns match-scoped team records categories.
|
|
258
|
+
func (s *TeamService) Records(ctx context.Context, teamQuery string, opts TeamLookupOptions) (NormalizedResult, error) {
|
|
259
|
+
lookup, passthrough := s.resolveTeamLookup(ctx, teamQuery, opts)
|
|
260
|
+
if passthrough != nil {
|
|
261
|
+
passthrough.Kind = EntityTeamRecords
|
|
262
|
+
return *passthrough, nil
|
|
263
|
+
}
|
|
264
|
+
if lookup.match == nil {
|
|
265
|
+
return matchScopeRequiredResult(EntityTeamRecords), nil
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
recordsRef := nonEmpty(strings.TrimSpace(lookup.team.RecordRef), competitorSubresourceRef(*lookup.match, lookup.team.ID, "records"))
|
|
269
|
+
if recordsRef == "" {
|
|
270
|
+
return NormalizedResult{Kind: EntityTeamRecords, Status: ResultStatusEmpty, Message: fmt.Sprintf("records route unavailable for team %q", lookup.team.ID)}, nil
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
resolved, err := s.client.ResolveRefChain(ctx, recordsRef)
|
|
274
|
+
if err != nil {
|
|
275
|
+
return NewTransportErrorResult(EntityTeamRecords, recordsRef, err), nil
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
categories, err := NormalizeTeamRecordCategories(resolved.Body)
|
|
279
|
+
if err != nil {
|
|
280
|
+
return NormalizedResult{}, fmt.Errorf("normalize team records %q: %w", resolved.CanonicalRef, err)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
items := make([]any, 0, len(categories))
|
|
284
|
+
for _, category := range categories {
|
|
285
|
+
items = append(items, category)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
result := NewListResult(EntityTeamRecords, items)
|
|
289
|
+
if len(lookup.warnings) > 0 {
|
|
290
|
+
result = NewPartialListResult(EntityTeamRecords, items, lookup.warnings...)
|
|
291
|
+
}
|
|
292
|
+
result.RequestedRef = resolved.RequestedRef
|
|
293
|
+
result.CanonicalRef = resolved.CanonicalRef
|
|
294
|
+
return result, nil
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
type teamLookup struct {
|
|
298
|
+
entity IndexedEntity
|
|
299
|
+
team Team
|
|
300
|
+
match *Match
|
|
301
|
+
teamResolved *ResolvedDocument
|
|
302
|
+
warnings []string
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
func (s *TeamService) resolveTeamLookup(ctx context.Context, teamQuery string, opts TeamLookupOptions) (*teamLookup, *NormalizedResult) {
|
|
306
|
+
teamQuery = strings.TrimSpace(teamQuery)
|
|
307
|
+
if teamQuery == "" {
|
|
308
|
+
result := NormalizedResult{Kind: EntityTeam, Status: ResultStatusEmpty, Message: "team query is required"}
|
|
309
|
+
return nil, &result
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
warnings := make([]string, 0)
|
|
313
|
+
var match *Match
|
|
314
|
+
if strings.TrimSpace(opts.MatchQuery) != "" {
|
|
315
|
+
resolvedMatch, matchWarnings, passthrough := s.resolveMatchContext(ctx, opts)
|
|
316
|
+
if passthrough != nil {
|
|
317
|
+
return nil, passthrough
|
|
318
|
+
}
|
|
319
|
+
match = resolvedMatch
|
|
320
|
+
warnings = append(warnings, matchWarnings...)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
searchResult, err := s.resolver.Search(ctx, EntityTeam, teamQuery, ResolveOptions{
|
|
324
|
+
Limit: 5,
|
|
325
|
+
LeagueID: strings.TrimSpace(opts.LeagueID),
|
|
326
|
+
MatchID: teamLookupMatchID(match),
|
|
327
|
+
})
|
|
328
|
+
if err != nil {
|
|
329
|
+
result := NewTransportErrorResult(EntityTeam, teamQuery, err)
|
|
330
|
+
return nil, &result
|
|
331
|
+
}
|
|
332
|
+
if len(searchResult.Entities) == 0 {
|
|
333
|
+
result := NormalizedResult{Kind: EntityTeam, Status: ResultStatusEmpty, Message: fmt.Sprintf("no teams found for %q", teamQuery)}
|
|
334
|
+
return nil, &result
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
warnings = append(warnings, searchResult.Warnings...)
|
|
338
|
+
entity := searchResult.Entities[0]
|
|
339
|
+
team, teamResolved, teamWarning := s.fetchGlobalTeam(ctx, entity)
|
|
340
|
+
if strings.TrimSpace(teamWarning) != "" {
|
|
341
|
+
warnings = append(warnings, teamWarning)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if match != nil {
|
|
345
|
+
if competitor := matchTeamByID(*match, entity.ID); competitor != nil {
|
|
346
|
+
team = mergeTeamViews(team, *competitor, *match)
|
|
347
|
+
} else {
|
|
348
|
+
warnings = append(warnings, fmt.Sprintf("team %s not found in match %s competitors", entity.ID, match.ID))
|
|
349
|
+
}
|
|
350
|
+
} else {
|
|
351
|
+
if team.Extensions == nil {
|
|
352
|
+
team.Extensions = map[string]any{}
|
|
353
|
+
}
|
|
354
|
+
team.Extensions["scope"] = string(TeamScopeGlobal)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return &teamLookup{
|
|
358
|
+
entity: entity,
|
|
359
|
+
team: team,
|
|
360
|
+
match: match,
|
|
361
|
+
teamResolved: teamResolved,
|
|
362
|
+
warnings: compactWarnings(warnings),
|
|
363
|
+
}, nil
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
func (s *TeamService) resolveMatchContext(ctx context.Context, opts TeamLookupOptions) (*Match, []string, *NormalizedResult) {
|
|
367
|
+
query := strings.TrimSpace(opts.MatchQuery)
|
|
368
|
+
if query == "" {
|
|
369
|
+
return nil, nil, nil
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
searchResult, err := s.resolver.Search(ctx, EntityMatch, query, ResolveOptions{Limit: 5, LeagueID: strings.TrimSpace(opts.LeagueID)})
|
|
373
|
+
if err != nil {
|
|
374
|
+
result := NewTransportErrorResult(EntityMatch, query, err)
|
|
375
|
+
return nil, nil, &result
|
|
376
|
+
}
|
|
377
|
+
if len(searchResult.Entities) == 0 {
|
|
378
|
+
result := NormalizedResult{Kind: EntityMatch, Status: ResultStatusEmpty, Message: fmt.Sprintf("no matches found for %q", query)}
|
|
379
|
+
return nil, nil, &result
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
entity := searchResult.Entities[0]
|
|
383
|
+
ref := buildMatchRef(entity)
|
|
384
|
+
if strings.TrimSpace(ref) == "" {
|
|
385
|
+
result := NormalizedResult{Kind: EntityMatch, Status: ResultStatusEmpty, Message: fmt.Sprintf("unable to resolve match ref for %q", query)}
|
|
386
|
+
return nil, nil, &result
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
resolved, err := s.client.ResolveRefChain(ctx, ref)
|
|
390
|
+
if err != nil {
|
|
391
|
+
result := NewTransportErrorResult(EntityMatch, ref, err)
|
|
392
|
+
return nil, nil, &result
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
match, err := NormalizeMatch(resolved.Body)
|
|
396
|
+
if err != nil {
|
|
397
|
+
result := NormalizedResult{Kind: EntityMatch, Status: ResultStatusError, Message: fmt.Sprintf("normalize competition match %q: %v", resolved.CanonicalRef, err)}
|
|
398
|
+
return nil, nil, &result
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
warnings := append([]string{}, searchResult.Warnings...)
|
|
402
|
+
return match, compactWarnings(warnings), nil
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
func (s *TeamService) fetchGlobalTeam(ctx context.Context, entity IndexedEntity) (Team, *ResolvedDocument, string) {
|
|
406
|
+
fallback := Team{
|
|
407
|
+
Ref: entity.Ref,
|
|
408
|
+
ID: entity.ID,
|
|
409
|
+
Name: entity.Name,
|
|
410
|
+
ShortName: entity.ShortName,
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
teamRef := strings.TrimSpace(entity.Ref)
|
|
414
|
+
if teamRef == "" || strings.Contains(teamRef, "/competitors/") {
|
|
415
|
+
teamRef = "/teams/" + strings.TrimSpace(entity.ID)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
resolved, err := s.client.ResolveRefChain(ctx, teamRef)
|
|
419
|
+
if err != nil {
|
|
420
|
+
return fallback, nil, fmt.Sprintf("team %s: %v", entity.ID, err)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
team, err := NormalizeTeam(resolved.Body)
|
|
424
|
+
if err != nil {
|
|
425
|
+
return fallback, resolved, fmt.Sprintf("team %s: %v", entity.ID, err)
|
|
426
|
+
}
|
|
427
|
+
if team.ID == "" {
|
|
428
|
+
team.ID = entity.ID
|
|
429
|
+
}
|
|
430
|
+
if team.Name == "" {
|
|
431
|
+
team.Name = entity.Name
|
|
432
|
+
}
|
|
433
|
+
if team.ShortName == "" {
|
|
434
|
+
team.ShortName = entity.ShortName
|
|
435
|
+
}
|
|
436
|
+
if team.Extensions == nil {
|
|
437
|
+
team.Extensions = map[string]any{}
|
|
438
|
+
}
|
|
439
|
+
team.Extensions["scope"] = string(TeamScopeGlobal)
|
|
440
|
+
|
|
441
|
+
return *team, resolved, ""
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
func (s *TeamService) enrichRosterEntries(entries []TeamRosterEntry) {
|
|
445
|
+
if s.resolver == nil || s.resolver.index == nil {
|
|
446
|
+
return
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
for i := range entries {
|
|
450
|
+
if strings.TrimSpace(entries[i].PlayerID) == "" {
|
|
451
|
+
continue
|
|
452
|
+
}
|
|
453
|
+
player, ok := s.resolver.index.FindByID(EntityPlayer, entries[i].PlayerID)
|
|
454
|
+
if !ok {
|
|
455
|
+
continue
|
|
456
|
+
}
|
|
457
|
+
if strings.TrimSpace(entries[i].DisplayName) == "" {
|
|
458
|
+
entries[i].DisplayName = nonEmpty(player.Name, player.ShortName)
|
|
459
|
+
}
|
|
460
|
+
if strings.TrimSpace(entries[i].PlayerRef) == "" {
|
|
461
|
+
entries[i].PlayerRef = strings.TrimSpace(player.Ref)
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
func (s *TeamService) enrichTeamLeaders(ctx context.Context, leaders *TeamLeaders) {
|
|
467
|
+
if leaders == nil || s.resolver == nil || s.resolver.index == nil {
|
|
468
|
+
return
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
for categoryIndex := range leaders.Categories {
|
|
472
|
+
for leaderIndex := range leaders.Categories[categoryIndex].Leaders {
|
|
473
|
+
entry := &leaders.Categories[categoryIndex].Leaders[leaderIndex]
|
|
474
|
+
if strings.TrimSpace(entry.AthleteID) == "" {
|
|
475
|
+
entry.AthleteID = strings.TrimSpace(refIDs(entry.AthleteRef)["athleteId"])
|
|
476
|
+
}
|
|
477
|
+
if strings.TrimSpace(entry.AthleteID) != "" {
|
|
478
|
+
if indexed, ok := s.resolver.index.FindByID(EntityPlayer, entry.AthleteID); !ok || strings.TrimSpace(nonEmpty(indexed.Name, indexed.ShortName)) == "" {
|
|
479
|
+
_ = s.resolver.seedPlayerByID(ctx, entry.AthleteID, "", strings.TrimSpace(leaders.MatchID))
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if strings.TrimSpace(entry.AthleteName) == "" || strings.EqualFold(strings.TrimSpace(entry.AthleteName), "Unknown player") {
|
|
483
|
+
if strings.TrimSpace(entry.AthleteID) == "" {
|
|
484
|
+
entry.AthleteName = "Unknown player"
|
|
485
|
+
continue
|
|
486
|
+
}
|
|
487
|
+
if player, ok := s.resolver.index.FindByID(EntityPlayer, entry.AthleteID); ok {
|
|
488
|
+
entry.AthleteName = nonEmpty(player.Name, player.ShortName, "Unknown player")
|
|
489
|
+
} else {
|
|
490
|
+
entry.AthleteName = "Unknown player"
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if strings.TrimSpace(entry.AthleteName) == "" {
|
|
494
|
+
entry.AthleteName = "Unknown player"
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
func matchScopeRequiredResult(kind EntityKind) NormalizedResult {
|
|
501
|
+
return NormalizedResult{
|
|
502
|
+
Kind: kind,
|
|
503
|
+
Status: ResultStatusEmpty,
|
|
504
|
+
Message: "match scope is required (use --match <match>)",
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
func competitorSubresourceRef(match Match, teamID, suffix string) string {
|
|
509
|
+
teamID = strings.TrimSpace(teamID)
|
|
510
|
+
suffix = strings.Trim(strings.TrimSpace(suffix), "/")
|
|
511
|
+
if teamID == "" {
|
|
512
|
+
return ""
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
base := matchSubresourceRef(match, "", "")
|
|
516
|
+
if base == "" {
|
|
517
|
+
return ""
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
ref := strings.TrimRight(base, "/") + "/competitors/" + teamID
|
|
521
|
+
if suffix != "" {
|
|
522
|
+
ref += "/" + suffix
|
|
523
|
+
}
|
|
524
|
+
return ref
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
func matchTeamByID(match Match, teamID string) *Team {
|
|
528
|
+
teamID = strings.TrimSpace(teamID)
|
|
529
|
+
if teamID == "" {
|
|
530
|
+
return nil
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
for i := range match.Teams {
|
|
534
|
+
candidate := &match.Teams[i]
|
|
535
|
+
ids := []string{
|
|
536
|
+
strings.TrimSpace(candidate.ID),
|
|
537
|
+
strings.TrimSpace(refIDs(candidate.Ref)["teamId"]),
|
|
538
|
+
strings.TrimSpace(refIDs(candidate.Ref)["competitorId"]),
|
|
539
|
+
}
|
|
540
|
+
for _, id := range ids {
|
|
541
|
+
if id != "" && id == teamID {
|
|
542
|
+
return candidate
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return nil
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
func mergeTeamViews(global Team, competitor Team, match Match) Team {
|
|
551
|
+
merged := global
|
|
552
|
+
if merged.Ref == "" {
|
|
553
|
+
merged.Ref = competitor.Ref
|
|
554
|
+
}
|
|
555
|
+
if merged.ID == "" {
|
|
556
|
+
merged.ID = competitor.ID
|
|
557
|
+
}
|
|
558
|
+
if merged.Name == "" {
|
|
559
|
+
merged.Name = competitor.Name
|
|
560
|
+
}
|
|
561
|
+
if merged.ShortName == "" {
|
|
562
|
+
merged.ShortName = competitor.ShortName
|
|
563
|
+
}
|
|
564
|
+
if merged.Abbreviation == "" {
|
|
565
|
+
merged.Abbreviation = competitor.Abbreviation
|
|
566
|
+
}
|
|
567
|
+
if competitor.ScoreSummary != "" {
|
|
568
|
+
merged.ScoreSummary = competitor.ScoreSummary
|
|
569
|
+
}
|
|
570
|
+
if competitor.Type != "" {
|
|
571
|
+
merged.Type = competitor.Type
|
|
572
|
+
}
|
|
573
|
+
if competitor.HomeAway != "" {
|
|
574
|
+
merged.HomeAway = competitor.HomeAway
|
|
575
|
+
}
|
|
576
|
+
if competitor.Order != 0 {
|
|
577
|
+
merged.Order = competitor.Order
|
|
578
|
+
}
|
|
579
|
+
merged.Winner = competitor.Winner
|
|
580
|
+
merged.ScoreRef = nonEmpty(competitor.ScoreRef, merged.ScoreRef)
|
|
581
|
+
merged.RosterRef = nonEmpty(competitor.RosterRef, merged.RosterRef)
|
|
582
|
+
merged.LeadersRef = nonEmpty(competitor.LeadersRef, merged.LeadersRef)
|
|
583
|
+
merged.StatisticsRef = nonEmpty(competitor.StatisticsRef, merged.StatisticsRef)
|
|
584
|
+
merged.RecordRef = nonEmpty(competitor.RecordRef, merged.RecordRef)
|
|
585
|
+
merged.LinescoresRef = nonEmpty(competitor.LinescoresRef, merged.LinescoresRef)
|
|
586
|
+
|
|
587
|
+
if merged.Extensions == nil {
|
|
588
|
+
merged.Extensions = map[string]any{}
|
|
589
|
+
}
|
|
590
|
+
merged.Extensions["scope"] = string(TeamScopeMatch)
|
|
591
|
+
merged.Extensions["matchId"] = match.ID
|
|
592
|
+
merged.Extensions["competitionId"] = match.CompetitionID
|
|
593
|
+
merged.Extensions["eventId"] = match.EventID
|
|
594
|
+
merged.Extensions["leagueId"] = match.LeagueID
|
|
595
|
+
return merged
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
func teamLookupMatchID(match *Match) string {
|
|
599
|
+
if match == nil {
|
|
600
|
+
return ""
|
|
601
|
+
}
|
|
602
|
+
return strings.TrimSpace(match.ID)
|
|
603
|
+
}
|