cricinfo-cli-go 0.1.2 → 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 +96 -0
- package/internal/cli/matches_test.go +71 -0
- package/internal/cli/search.go +156 -0
- package/internal/cricinfo/analysis.go +177 -47
- 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/matches.go +1036 -22
- package/internal/cricinfo/matches_phase7_test.go +11 -4
- package/internal/cricinfo/normalize_entities.go +67 -35
- package/internal/cricinfo/players.go +236 -2
- package/internal/cricinfo/render_contract.go +139 -37
- package/internal/cricinfo/renderer.go +422 -13
- package/internal/cricinfo/resolver.go +92 -15
- 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
|
|
|
@@ -413,36 +461,34 @@ func (r *Resolver) seedCompetitionMap(ctx context.Context, comp map[string]any,
|
|
|
413
461
|
}
|
|
414
462
|
|
|
415
463
|
func (r *Resolver) matchTeamAliasesFromCompetitor(ctx context.Context, competitor map[string]any, leagueID, matchID string) []string {
|
|
464
|
+
team := mapField(competitor, "team")
|
|
416
465
|
aliases := []string{
|
|
417
|
-
stringField(
|
|
418
|
-
stringField(
|
|
419
|
-
stringField(
|
|
420
|
-
stringField(
|
|
421
|
-
stringField(
|
|
466
|
+
stringField(team, "displayName"),
|
|
467
|
+
stringField(team, "name"),
|
|
468
|
+
stringField(team, "shortDisplayName"),
|
|
469
|
+
stringField(team, "shortName"),
|
|
470
|
+
stringField(team, "abbreviation"),
|
|
422
471
|
}
|
|
423
472
|
|
|
424
473
|
teamRef := refFromField(competitor, "team")
|
|
425
474
|
teamID := nonEmpty(
|
|
426
475
|
refIDs(teamRef)["teamId"],
|
|
427
|
-
stringField(
|
|
476
|
+
stringField(team, "id"),
|
|
428
477
|
stringField(competitor, "id"),
|
|
429
478
|
)
|
|
430
479
|
if teamID == "" {
|
|
431
|
-
return aliases
|
|
480
|
+
return expandKnownTeamAliases(aliases)
|
|
432
481
|
}
|
|
433
482
|
|
|
434
483
|
if existing, ok := r.index.FindByID(EntityTeam, teamID); ok {
|
|
435
484
|
aliases = append(aliases, existing.Name, existing.ShortName)
|
|
436
485
|
}
|
|
437
|
-
if hasNonEmptyAlias(aliases...) {
|
|
438
|
-
return aliases
|
|
439
|
-
}
|
|
440
486
|
|
|
441
487
|
_ = r.seedTeamByID(ctx, teamID, leagueID, matchID)
|
|
442
488
|
if existing, ok := r.index.FindByID(EntityTeam, teamID); ok {
|
|
443
489
|
aliases = append(aliases, existing.Name, existing.ShortName)
|
|
444
490
|
}
|
|
445
|
-
return aliases
|
|
491
|
+
return expandKnownTeamAliases(aliases)
|
|
446
492
|
}
|
|
447
493
|
|
|
448
494
|
func hasNonEmptyAlias(values ...string) bool {
|
|
@@ -454,6 +500,37 @@ func hasNonEmptyAlias(values ...string) bool {
|
|
|
454
500
|
return false
|
|
455
501
|
}
|
|
456
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
|
+
|
|
457
534
|
func (r *Resolver) seedCompetitorMap(ctx context.Context, competitor map[string]any, leagueID, eventID, matchID string) error {
|
|
458
535
|
competitorRef := stringField(competitor, "$ref")
|
|
459
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) {
|