cricinfo-cli-go 0.1.1 → 0.1.4
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/internal/cli/matches.go +126 -2
- package/internal/cli/matches_test.go +82 -0
- package/internal/cli/search.go +156 -0
- package/internal/cricinfo/analysis.go +393 -93
- package/internal/cricinfo/analysis_phase15_test.go +38 -0
- package/internal/cricinfo/client.go +23 -2
- package/internal/cricinfo/coverage_ledger_test.go +2 -22
- package/internal/cricinfo/entity_index.go +27 -0
- package/internal/cricinfo/historical_hydration.go +82 -42
- package/internal/cricinfo/matches.go +1641 -88
- package/internal/cricinfo/matches_phase7_test.go +11 -4
- package/internal/cricinfo/normalize_entities.go +83 -35
- package/internal/cricinfo/players.go +236 -2
- package/internal/cricinfo/render_contract.go +191 -49
- package/internal/cricinfo/renderer.go +613 -19
- package/internal/cricinfo/resolver.go +134 -13
- package/internal/cricinfo/teams.go +109 -6
- package/internal/cricinfo/testdata/coverage/cricinfo-field-path-catalog.txt +2536 -0
- package/internal/cricinfo/testdata/coverage/cricinfo-working-templates.tsv +56 -0
- package/package.json +1 -1
|
@@ -131,6 +131,15 @@ func (r *Resolver) Search(ctx context.Context, kind EntityKind, query string, op
|
|
|
131
131
|
PreferredLeagueID: strings.TrimSpace(opts.LeagueID),
|
|
132
132
|
PreferredMatchID: strings.TrimSpace(opts.MatchID),
|
|
133
133
|
})
|
|
134
|
+
if kind == EntityMatch && strings.TrimSpace(query) != "" && len(entities) == 0 {
|
|
135
|
+
if err := r.seedFromEventsFresh(ctx); err != nil {
|
|
136
|
+
warnings = append(warnings, err.Error())
|
|
137
|
+
}
|
|
138
|
+
entities = r.index.Search(kind, query, limit, SearchContext{
|
|
139
|
+
PreferredLeagueID: strings.TrimSpace(opts.LeagueID),
|
|
140
|
+
PreferredMatchID: strings.TrimSpace(opts.MatchID),
|
|
141
|
+
})
|
|
142
|
+
}
|
|
134
143
|
|
|
135
144
|
_ = r.index.Persist()
|
|
136
145
|
|
|
@@ -166,8 +175,16 @@ func (r *Resolver) seedContext(ctx context.Context, opts ResolveOptions) error {
|
|
|
166
175
|
}
|
|
167
176
|
|
|
168
177
|
func (r *Resolver) seedFromEvents(ctx context.Context) error {
|
|
178
|
+
return r.seedFromEventsWithTTL(ctx, true)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
func (r *Resolver) seedFromEventsFresh(ctx context.Context) error {
|
|
182
|
+
return r.seedFromEventsWithTTL(ctx, false)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
func (r *Resolver) seedFromEventsWithTTL(ctx context.Context, respectTTL bool) error {
|
|
169
186
|
last := r.index.LastEventsSeedAt()
|
|
170
|
-
if !last.IsZero() && r.now().Sub(last) < r.eventSeedTTL {
|
|
187
|
+
if respectTTL && !last.IsZero() && r.now().Sub(last) < r.eventSeedTTL {
|
|
171
188
|
return nil
|
|
172
189
|
}
|
|
173
190
|
|
|
@@ -188,7 +205,11 @@ func (r *Resolver) seedFromEvents(ctx context.Context) error {
|
|
|
188
205
|
successCount := 0
|
|
189
206
|
seedErrors := make([]string, 0)
|
|
190
207
|
for i := 0; i < limit; i++ {
|
|
191
|
-
|
|
208
|
+
seedFn := r.seedEventRef
|
|
209
|
+
if !respectTTL {
|
|
210
|
+
seedFn = r.seedEventRefFresh
|
|
211
|
+
}
|
|
212
|
+
if err := seedFn(ctx, page.Items[i].URL); err != nil {
|
|
192
213
|
seedErrors = append(seedErrors, fmt.Sprintf("%s (%v)", page.Items[i].URL, err))
|
|
193
214
|
continue
|
|
194
215
|
}
|
|
@@ -266,11 +287,19 @@ func (r *Resolver) seedNumericID(ctx context.Context, kind EntityKind, id string
|
|
|
266
287
|
}
|
|
267
288
|
|
|
268
289
|
func (r *Resolver) seedEventRef(ctx context.Context, ref string) error {
|
|
290
|
+
return r.seedEventRefWithHydration(ctx, ref, true)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
func (r *Resolver) seedEventRefFresh(ctx context.Context, ref string) error {
|
|
294
|
+
return r.seedEventRefWithHydration(ctx, ref, false)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
func (r *Resolver) seedEventRefWithHydration(ctx context.Context, ref string, checkHydrated bool) error {
|
|
269
298
|
ref = r.absoluteRef(ref)
|
|
270
299
|
if ref == "" {
|
|
271
300
|
return fmt.Errorf("empty event ref")
|
|
272
301
|
}
|
|
273
|
-
if r.isHydrated(ref) {
|
|
302
|
+
if checkHydrated && r.isHydrated(ref) {
|
|
274
303
|
return nil
|
|
275
304
|
}
|
|
276
305
|
|
|
@@ -317,6 +346,17 @@ func (r *Resolver) seedEventRef(ctx context.Context, ref string) error {
|
|
|
317
346
|
|
|
318
347
|
competitions := mapSliceField(payload, "competitions")
|
|
319
348
|
for _, comp := range competitions {
|
|
349
|
+
compRef := stringField(comp, "$ref")
|
|
350
|
+
if compRef != "" && len(mapSliceField(comp, "competitors")) == 0 {
|
|
351
|
+
seedCompetitionFn := r.seedCompetitionRef
|
|
352
|
+
if !checkHydrated {
|
|
353
|
+
seedCompetitionFn = r.seedCompetitionRefFresh
|
|
354
|
+
}
|
|
355
|
+
if err := seedCompetitionFn(ctx, compRef); err != nil {
|
|
356
|
+
return err
|
|
357
|
+
}
|
|
358
|
+
continue
|
|
359
|
+
}
|
|
320
360
|
if err := r.seedCompetitionMap(ctx, comp, leagueID, eventID, eventName); err != nil {
|
|
321
361
|
return err
|
|
322
362
|
}
|
|
@@ -329,11 +369,19 @@ func (r *Resolver) seedEventRef(ctx context.Context, ref string) error {
|
|
|
329
369
|
}
|
|
330
370
|
|
|
331
371
|
func (r *Resolver) seedCompetitionRef(ctx context.Context, ref string) error {
|
|
372
|
+
return r.seedCompetitionRefWithHydration(ctx, ref, true)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
func (r *Resolver) seedCompetitionRefFresh(ctx context.Context, ref string) error {
|
|
376
|
+
return r.seedCompetitionRefWithHydration(ctx, ref, false)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
func (r *Resolver) seedCompetitionRefWithHydration(ctx context.Context, ref string, checkHydrated bool) error {
|
|
332
380
|
ref = r.absoluteRef(ref)
|
|
333
381
|
if ref == "" {
|
|
334
382
|
return fmt.Errorf("empty competition ref")
|
|
335
383
|
}
|
|
336
|
-
if r.isHydrated(ref) {
|
|
384
|
+
if checkHydrated && r.isHydrated(ref) {
|
|
337
385
|
return nil
|
|
338
386
|
}
|
|
339
387
|
|
|
@@ -375,6 +423,15 @@ func (r *Resolver) seedCompetitionMap(ctx context.Context, comp map[string]any,
|
|
|
375
423
|
matchName := nonEmpty(stringField(comp, "shortDescription"), stringField(comp, "description"), stringField(comp, "note"), eventName, stringField(comp, "date"))
|
|
376
424
|
matchShort := nonEmpty(stringField(comp, "shortDescription"), eventName)
|
|
377
425
|
|
|
426
|
+
competitors := mapSliceField(comp, "competitors")
|
|
427
|
+
teamAliases := make([]string, 0)
|
|
428
|
+
for _, competitor := range competitors {
|
|
429
|
+
if err := r.seedCompetitorMap(ctx, competitor, leagueID, eventID, competitionID); err != nil {
|
|
430
|
+
return err
|
|
431
|
+
}
|
|
432
|
+
teamAliases = append(teamAliases, r.matchTeamAliasesFromCompetitor(ctx, competitor, leagueID, competitionID)...)
|
|
433
|
+
}
|
|
434
|
+
|
|
378
435
|
if err := r.index.Upsert(IndexedEntity{
|
|
379
436
|
Kind: EntityMatch,
|
|
380
437
|
ID: competitionID,
|
|
@@ -384,32 +441,96 @@ func (r *Resolver) seedCompetitionMap(ctx context.Context, comp map[string]any,
|
|
|
384
441
|
LeagueID: leagueID,
|
|
385
442
|
EventID: eventID,
|
|
386
443
|
MatchID: competitionID,
|
|
387
|
-
Aliases: []string{
|
|
444
|
+
Aliases: append([]string{
|
|
388
445
|
stringField(comp, "description"),
|
|
389
446
|
stringField(comp, "shortDescription"),
|
|
390
447
|
stringField(comp, "note"),
|
|
391
448
|
eventName,
|
|
392
449
|
competitionID,
|
|
393
450
|
eventID,
|
|
394
|
-
},
|
|
451
|
+
}, teamAliases...),
|
|
395
452
|
UpdatedAt: r.now(),
|
|
396
453
|
}); err != nil {
|
|
397
454
|
return err
|
|
398
455
|
}
|
|
399
456
|
|
|
400
|
-
competitors := mapSliceField(comp, "competitors")
|
|
401
|
-
for _, competitor := range competitors {
|
|
402
|
-
if err := r.seedCompetitorMap(ctx, competitor, leagueID, eventID, competitionID); err != nil {
|
|
403
|
-
return err
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
457
|
if compRef != "" {
|
|
408
458
|
r.markHydrated(r.absoluteRef(compRef))
|
|
409
459
|
}
|
|
410
460
|
return nil
|
|
411
461
|
}
|
|
412
462
|
|
|
463
|
+
func (r *Resolver) matchTeamAliasesFromCompetitor(ctx context.Context, competitor map[string]any, leagueID, matchID string) []string {
|
|
464
|
+
team := mapField(competitor, "team")
|
|
465
|
+
aliases := []string{
|
|
466
|
+
stringField(team, "displayName"),
|
|
467
|
+
stringField(team, "name"),
|
|
468
|
+
stringField(team, "shortDisplayName"),
|
|
469
|
+
stringField(team, "shortName"),
|
|
470
|
+
stringField(team, "abbreviation"),
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
teamRef := refFromField(competitor, "team")
|
|
474
|
+
teamID := nonEmpty(
|
|
475
|
+
refIDs(teamRef)["teamId"],
|
|
476
|
+
stringField(team, "id"),
|
|
477
|
+
stringField(competitor, "id"),
|
|
478
|
+
)
|
|
479
|
+
if teamID == "" {
|
|
480
|
+
return expandKnownTeamAliases(aliases)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if existing, ok := r.index.FindByID(EntityTeam, teamID); ok {
|
|
484
|
+
aliases = append(aliases, existing.Name, existing.ShortName)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
_ = r.seedTeamByID(ctx, teamID, leagueID, matchID)
|
|
488
|
+
if existing, ok := r.index.FindByID(EntityTeam, teamID); ok {
|
|
489
|
+
aliases = append(aliases, existing.Name, existing.ShortName)
|
|
490
|
+
}
|
|
491
|
+
return expandKnownTeamAliases(aliases)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
func hasNonEmptyAlias(values ...string) bool {
|
|
495
|
+
for _, value := range values {
|
|
496
|
+
if strings.TrimSpace(value) != "" {
|
|
497
|
+
return true
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return false
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
func expandKnownTeamAliases(values []string) []string {
|
|
504
|
+
expanded := append([]string{}, values...)
|
|
505
|
+
seen := map[string]struct{}{}
|
|
506
|
+
for _, value := range values {
|
|
507
|
+
normalized := normalizeAlias(value)
|
|
508
|
+
if normalized == "" {
|
|
509
|
+
continue
|
|
510
|
+
}
|
|
511
|
+
seen[normalized] = struct{}{}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
add := func(value string) {
|
|
515
|
+
normalized := normalizeAlias(value)
|
|
516
|
+
if normalized == "" {
|
|
517
|
+
return
|
|
518
|
+
}
|
|
519
|
+
if _, ok := seen[normalized]; ok {
|
|
520
|
+
return
|
|
521
|
+
}
|
|
522
|
+
seen[normalized] = struct{}{}
|
|
523
|
+
expanded = append(expanded, value)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
for _, value := range values {
|
|
527
|
+
for _, alias := range knownIPLTeamAliases[normalizeAlias(value)] {
|
|
528
|
+
add(alias)
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return expanded
|
|
532
|
+
}
|
|
533
|
+
|
|
413
534
|
func (r *Resolver) seedCompetitorMap(ctx context.Context, competitor map[string]any, leagueID, eventID, matchID string) error {
|
|
414
535
|
competitorRef := stringField(competitor, "$ref")
|
|
415
536
|
teamRef := refFromField(competitor, "team")
|
|
@@ -4,8 +4,13 @@ import (
|
|
|
4
4
|
"context"
|
|
5
5
|
"fmt"
|
|
6
6
|
"strings"
|
|
7
|
+
"sync"
|
|
8
|
+
"time"
|
|
7
9
|
)
|
|
8
10
|
|
|
11
|
+
const teamRosterNameHydrationConcurrency = 8
|
|
12
|
+
const teamRosterNameHydrationTimeout = 2500 * time.Millisecond
|
|
13
|
+
|
|
9
14
|
// TeamServiceConfig configures team discovery and match-scoped team commands.
|
|
10
15
|
type TeamServiceConfig struct {
|
|
11
16
|
Client *Client
|
|
@@ -125,7 +130,7 @@ func (s *TeamService) Roster(ctx context.Context, teamQuery string, opts TeamLoo
|
|
|
125
130
|
return NormalizedResult{}, fmt.Errorf("normalize team roster %q: %w", resolved.CanonicalRef, err)
|
|
126
131
|
}
|
|
127
132
|
|
|
128
|
-
s.enrichRosterEntries(entries)
|
|
133
|
+
warnings = append(warnings, s.enrichRosterEntries(ctx, entries)...)
|
|
129
134
|
|
|
130
135
|
items := make([]any, 0, len(entries))
|
|
131
136
|
for _, entry := range entries {
|
|
@@ -441,26 +446,124 @@ func (s *TeamService) fetchGlobalTeam(ctx context.Context, entity IndexedEntity)
|
|
|
441
446
|
return *team, resolved, ""
|
|
442
447
|
}
|
|
443
448
|
|
|
444
|
-
func (s *TeamService) enrichRosterEntries(entries []TeamRosterEntry) {
|
|
449
|
+
func (s *TeamService) enrichRosterEntries(ctx context.Context, entries []TeamRosterEntry) []string {
|
|
445
450
|
if s.resolver == nil || s.resolver.index == nil {
|
|
446
|
-
return
|
|
451
|
+
return nil
|
|
447
452
|
}
|
|
448
453
|
|
|
454
|
+
missing := make([]int, 0)
|
|
449
455
|
for i := range entries {
|
|
450
|
-
|
|
456
|
+
playerID := strings.TrimSpace(entries[i].PlayerID)
|
|
457
|
+
if playerID == "" {
|
|
458
|
+
playerID = strings.TrimSpace(refIDs(entries[i].PlayerRef)["athleteId"])
|
|
459
|
+
entries[i].PlayerID = playerID
|
|
460
|
+
}
|
|
461
|
+
if playerID == "" {
|
|
451
462
|
continue
|
|
452
463
|
}
|
|
453
|
-
player, ok := s.resolver.index.FindByID(EntityPlayer,
|
|
464
|
+
player, ok := s.resolver.index.FindByID(EntityPlayer, playerID)
|
|
454
465
|
if !ok {
|
|
466
|
+
missing = append(missing, i)
|
|
455
467
|
continue
|
|
456
468
|
}
|
|
457
469
|
if strings.TrimSpace(entries[i].DisplayName) == "" {
|
|
458
|
-
entries[i].DisplayName = nonEmpty(player.Name, player.ShortName)
|
|
470
|
+
entries[i].DisplayName = nonEmpty(player.Name, player.ShortName, player.ID)
|
|
459
471
|
}
|
|
460
472
|
if strings.TrimSpace(entries[i].PlayerRef) == "" {
|
|
461
473
|
entries[i].PlayerRef = strings.TrimSpace(player.Ref)
|
|
462
474
|
}
|
|
463
475
|
}
|
|
476
|
+
|
|
477
|
+
if len(missing) == 0 {
|
|
478
|
+
return nil
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
type hydrateResult struct {
|
|
482
|
+
index int
|
|
483
|
+
displayName string
|
|
484
|
+
playerRef string
|
|
485
|
+
warning string
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
results := make([]hydrateResult, len(missing))
|
|
489
|
+
sem := make(chan struct{}, teamRosterNameHydrationConcurrency)
|
|
490
|
+
var wg sync.WaitGroup
|
|
491
|
+
|
|
492
|
+
for resultIndex, entryIndex := range missing {
|
|
493
|
+
wg.Add(1)
|
|
494
|
+
go func(resultIndex, entryIndex int) {
|
|
495
|
+
defer wg.Done()
|
|
496
|
+
sem <- struct{}{}
|
|
497
|
+
defer func() { <-sem }()
|
|
498
|
+
|
|
499
|
+
entry := entries[entryIndex]
|
|
500
|
+
playerID := strings.TrimSpace(entry.PlayerID)
|
|
501
|
+
playerRef := strings.TrimSpace(entry.PlayerRef)
|
|
502
|
+
if playerID == "" {
|
|
503
|
+
playerID = strings.TrimSpace(refIDs(playerRef)["athleteId"])
|
|
504
|
+
}
|
|
505
|
+
if playerID == "" && playerRef == "" {
|
|
506
|
+
results[resultIndex] = hydrateResult{
|
|
507
|
+
index: entryIndex,
|
|
508
|
+
warning: "roster entry missing player id/ref",
|
|
509
|
+
}
|
|
510
|
+
return
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
reqCtx, cancel := context.WithTimeout(ctx, teamRosterNameHydrationTimeout)
|
|
514
|
+
var seedErr error
|
|
515
|
+
if playerID != "" {
|
|
516
|
+
seedErr = s.resolver.seedPlayerByID(reqCtx, playerID, "", strings.TrimSpace(entry.MatchID))
|
|
517
|
+
} else {
|
|
518
|
+
seedErr = s.resolver.seedPlayerRef(reqCtx, playerRef, "", strings.TrimSpace(entry.MatchID))
|
|
519
|
+
}
|
|
520
|
+
cancel()
|
|
521
|
+
|
|
522
|
+
if playerID == "" {
|
|
523
|
+
playerID = strings.TrimSpace(refIDs(playerRef)["athleteId"])
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
player := IndexedEntity{}
|
|
527
|
+
ok := false
|
|
528
|
+
if playerID != "" {
|
|
529
|
+
player, ok = s.resolver.index.FindByID(EntityPlayer, playerID)
|
|
530
|
+
}
|
|
531
|
+
if !ok && playerRef != "" {
|
|
532
|
+
player, ok = s.resolver.index.FindByRef(EntityPlayer, playerRef)
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
result := hydrateResult{index: entryIndex}
|
|
536
|
+
if ok {
|
|
537
|
+
result.displayName = nonEmpty(strings.TrimSpace(player.Name), strings.TrimSpace(player.ShortName), strings.TrimSpace(player.ID))
|
|
538
|
+
result.playerRef = strings.TrimSpace(player.Ref)
|
|
539
|
+
}
|
|
540
|
+
if seedErr != nil && result.displayName == "" {
|
|
541
|
+
label := nonEmpty(playerID, playerRef, fmt.Sprintf("entry-%d", entryIndex+1))
|
|
542
|
+
result.warning = fmt.Sprintf("player %s: %v", label, seedErr)
|
|
543
|
+
}
|
|
544
|
+
results[resultIndex] = result
|
|
545
|
+
}(resultIndex, entryIndex)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
wg.Wait()
|
|
549
|
+
|
|
550
|
+
warnings := make([]string, 0)
|
|
551
|
+
for _, result := range results {
|
|
552
|
+
if result.index < 0 || result.index >= len(entries) {
|
|
553
|
+
continue
|
|
554
|
+
}
|
|
555
|
+
if strings.TrimSpace(entries[result.index].DisplayName) == "" && strings.TrimSpace(result.displayName) != "" {
|
|
556
|
+
entries[result.index].DisplayName = result.displayName
|
|
557
|
+
}
|
|
558
|
+
if strings.TrimSpace(entries[result.index].PlayerRef) == "" && strings.TrimSpace(result.playerRef) != "" {
|
|
559
|
+
entries[result.index].PlayerRef = result.playerRef
|
|
560
|
+
}
|
|
561
|
+
if result.warning != "" {
|
|
562
|
+
warnings = append(warnings, result.warning)
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return compactWarnings(warnings)
|
|
464
567
|
}
|
|
465
568
|
|
|
466
569
|
func (s *TeamService) enrichTeamLeaders(ctx context.Context, leaders *TeamLeaders) {
|