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.
Files changed (121) hide show
  1. package/AGENTS.md +63 -0
  2. package/CONTRIBUTORS.md +75 -0
  3. package/LICENSE +21 -0
  4. package/Makefile +131 -0
  5. package/README.md +130 -0
  6. package/bin/cricinfo.js +44 -0
  7. package/cmd/cricinfo/main.go +15 -0
  8. package/go.mod +10 -0
  9. package/go.sum +10 -0
  10. package/internal/app/app.go +11 -0
  11. package/internal/app/app_test.go +122 -0
  12. package/internal/buildinfo/buildinfo.go +16 -0
  13. package/internal/cli/analysis.go +262 -0
  14. package/internal/cli/analysis_test.go +175 -0
  15. package/internal/cli/competitions.go +154 -0
  16. package/internal/cli/competitions_test.go +165 -0
  17. package/internal/cli/leagues.go +297 -0
  18. package/internal/cli/leagues_test.go +194 -0
  19. package/internal/cli/matches.go +403 -0
  20. package/internal/cli/matches_test.go +413 -0
  21. package/internal/cli/players.go +263 -0
  22. package/internal/cli/players_test.go +384 -0
  23. package/internal/cli/root.go +141 -0
  24. package/internal/cli/search.go +119 -0
  25. package/internal/cli/teams.go +214 -0
  26. package/internal/cli/teams_test.go +192 -0
  27. package/internal/cricinfo/analysis.go +1401 -0
  28. package/internal/cricinfo/analysis_phase15_test.go +267 -0
  29. package/internal/cricinfo/client.go +471 -0
  30. package/internal/cricinfo/client_test.go +280 -0
  31. package/internal/cricinfo/cmd/fixture-refresh/main.go +145 -0
  32. package/internal/cricinfo/competitions.go +405 -0
  33. package/internal/cricinfo/competitions_phase13_test.go +234 -0
  34. package/internal/cricinfo/coverage_ledger.go +122 -0
  35. package/internal/cricinfo/coverage_ledger_test.go +253 -0
  36. package/internal/cricinfo/decode.go +115 -0
  37. package/internal/cricinfo/decode_test.go +100 -0
  38. package/internal/cricinfo/entity_index.go +618 -0
  39. package/internal/cricinfo/entity_index_test.go +175 -0
  40. package/internal/cricinfo/fixture_matrix.go +243 -0
  41. package/internal/cricinfo/fixture_matrix_test.go +49 -0
  42. package/internal/cricinfo/fixtures_test.go +264 -0
  43. package/internal/cricinfo/historical_hydration.go +1641 -0
  44. package/internal/cricinfo/historical_phase14_test.go +542 -0
  45. package/internal/cricinfo/leagues.go +1210 -0
  46. package/internal/cricinfo/leagues_phase12_test.go +324 -0
  47. package/internal/cricinfo/live_leagues_test.go +169 -0
  48. package/internal/cricinfo/live_matches_test.go +203 -0
  49. package/internal/cricinfo/live_matrix_test.go +118 -0
  50. package/internal/cricinfo/live_players_test.go +122 -0
  51. package/internal/cricinfo/live_search_test.go +86 -0
  52. package/internal/cricinfo/live_smoke_test.go +213 -0
  53. package/internal/cricinfo/live_teams_test.go +104 -0
  54. package/internal/cricinfo/matches.go +1508 -0
  55. package/internal/cricinfo/matches_phase7_test.go +207 -0
  56. package/internal/cricinfo/matches_phase9_test.go +253 -0
  57. package/internal/cricinfo/normalize_entities.go +1727 -0
  58. package/internal/cricinfo/normalize_leagues.go +346 -0
  59. package/internal/cricinfo/players.go +1332 -0
  60. package/internal/cricinfo/players_phase10_test.go +174 -0
  61. package/internal/cricinfo/players_phase11_test.go +373 -0
  62. package/internal/cricinfo/render_contract.go +1088 -0
  63. package/internal/cricinfo/render_phase4_test.go +633 -0
  64. package/internal/cricinfo/renderer.go +1689 -0
  65. package/internal/cricinfo/resolver.go +813 -0
  66. package/internal/cricinfo/resolver_test.go +244 -0
  67. package/internal/cricinfo/teams.go +603 -0
  68. package/internal/cricinfo/teams_phase8_test.go +231 -0
  69. package/internal/cricinfo/testdata/fixtures/README.md +43 -0
  70. package/internal/cricinfo/testdata/fixtures/aux-competition-metadata/broadcasts.json +11 -0
  71. package/internal/cricinfo/testdata/fixtures/aux-competition-metadata/officials.json +150 -0
  72. package/internal/cricinfo/testdata/fixtures/details-plays/detail-110.json +157 -0
  73. package/internal/cricinfo/testdata/fixtures/details-plays/detail-52545007.json +145 -0
  74. package/internal/cricinfo/testdata/fixtures/details-plays/detail-52559021.json +143 -0
  75. package/internal/cricinfo/testdata/fixtures/details-plays/plays.json +15 -0
  76. package/internal/cricinfo/testdata/fixtures/endpoint-matrix.tsv +19 -0
  77. package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/fow-1.json +12 -0
  78. package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/fow.json +42 -0
  79. package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/innings-1-2.json +38 -0
  80. package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/partnership-1.json +31 -0
  81. package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/partnerships.json +42 -0
  82. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/calendar-offdays.json +20 -0
  83. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/calendar-ondays.json +21 -0
  84. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/calendar.json +14 -0
  85. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-2025.json +13 -0
  86. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-group-1.json +13 -0
  87. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-groups.json +11 -0
  88. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-type-1.json +13 -0
  89. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-types.json +11 -0
  90. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/seasons.json +30 -0
  91. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/standings-item-1.json +72 -0
  92. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/standings-root.json +3 -0
  93. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/standings.json +15 -0
  94. package/internal/cricinfo/testdata/fixtures/matches-competitions/competition.json +460 -0
  95. package/internal/cricinfo/testdata/fixtures/matches-competitions/event-1529474.json +86 -0
  96. package/internal/cricinfo/testdata/fixtures/matches-competitions/matchcards-1527966.json +368 -0
  97. package/internal/cricinfo/testdata/fixtures/matches-competitions/situation-1529474.json +10 -0
  98. package/internal/cricinfo/testdata/fixtures/players/athlete-1361257-statistics.json +126 -0
  99. package/internal/cricinfo/testdata/fixtures/players/athlete-1361257.json +113 -0
  100. package/internal/cricinfo/testdata/fixtures/players/roster-1361257-linescores-1-1-statistics-0.json +208 -0
  101. package/internal/cricinfo/testdata/fixtures/players/roster-1361257-linescores-1-2-statistics-0.json +252 -0
  102. package/internal/cricinfo/testdata/fixtures/players/roster-1361257-linescores.json +74 -0
  103. package/internal/cricinfo/testdata/fixtures/players/roster-1361257-statistics-0.json +1008 -0
  104. package/internal/cricinfo/testdata/fixtures/root-discovery/events.json +72 -0
  105. package/internal/cricinfo/testdata/fixtures/root-discovery/root.json +28 -0
  106. package/internal/cricinfo/testdata/fixtures/team-competitor/competitor-789643.json +40 -0
  107. package/internal/cricinfo/testdata/fixtures/team-competitor/leaders-789643.json +353 -0
  108. package/internal/cricinfo/testdata/fixtures/team-competitor/records-789643.json +91 -0
  109. package/internal/cricinfo/testdata/fixtures/team-competitor/roster-1147772-object.json +231 -0
  110. package/internal/cricinfo/testdata/fixtures/team-competitor/roster-1147772.json +235 -0
  111. package/internal/cricinfo/testdata/fixtures/team-competitor/roster-789643.json +322 -0
  112. package/internal/cricinfo/testdata/fixtures/team-competitor/scores-789643.json +19 -0
  113. package/internal/cricinfo/testdata/fixtures/team-competitor/statistics-789643.json +629 -0
  114. package/internal/cricinfo/testdata/fixtures/team-competitor/team-789643-athletes.json +7 -0
  115. package/internal/cricinfo/testdata/fixtures/team-competitor/team-789643.json +67 -0
  116. package/internal/cricinfo/testdata/golden/match-empty.golden +1 -0
  117. package/internal/cricinfo/testdata/golden/match-list.golden +2 -0
  118. package/internal/cricinfo/testdata/golden/match-partial.golden +3 -0
  119. package/internal/cricinfo/types.go +54 -0
  120. package/package.json +51 -0
  121. package/scripts/postinstall.js +153 -0
@@ -0,0 +1,346 @@
1
+ package cricinfo
2
+
3
+ import (
4
+ "encoding/json"
5
+ "strings"
6
+ )
7
+
8
+ // NormalizeSeason maps a season payload into the normalized season shape.
9
+ func NormalizeSeason(data []byte) (*Season, error) {
10
+ payload, err := decodePayloadMap(data)
11
+ if err != nil {
12
+ return nil, err
13
+ }
14
+ return NormalizeSeasonFromMap(payload), nil
15
+ }
16
+
17
+ // NormalizeSeasonFromMap maps a decoded season payload into the normalized season shape.
18
+ func NormalizeSeasonFromMap(payload map[string]any) *Season {
19
+ ref := stringField(payload, "$ref")
20
+ ids := refIDs(ref)
21
+ seasonID := nonEmpty(ids["seasonId"], stringField(payload, "year"))
22
+
23
+ return &Season{
24
+ Ref: ref,
25
+ ID: seasonID,
26
+ LeagueID: nonEmpty(ids["leagueId"], stringField(payload, "id")),
27
+ Year: firstNonZeroValue(intField(payload, "year"), parseYear(seasonID)),
28
+ Extensions: extensionsFromMap(payload,
29
+ "$ref", "id", "year",
30
+ ),
31
+ }
32
+ }
33
+
34
+ // NormalizeSeasonType maps a season-type payload into the normalized season type shape.
35
+ func NormalizeSeasonType(data []byte) (*SeasonType, error) {
36
+ payload, err := decodePayloadMap(data)
37
+ if err != nil {
38
+ return nil, err
39
+ }
40
+
41
+ ref := stringField(payload, "$ref")
42
+ ids := refIDs(ref)
43
+ seasonRef := refFromField(payload, "season")
44
+ seasonIDs := refIDs(seasonRef)
45
+
46
+ return &SeasonType{
47
+ Ref: ref,
48
+ ID: nonEmpty(stringField(payload, "id"), ids["typeId"]),
49
+ LeagueID: nonEmpty(ids["leagueId"], seasonIDs["leagueId"]),
50
+ SeasonID: nonEmpty(ids["seasonId"], seasonIDs["seasonId"]),
51
+ Name: nonEmpty(stringField(payload, "name"), stringField(payload, "displayName")),
52
+ Abbreviation: stringField(payload, "abbreviation"),
53
+ StartDate: stringField(payload, "startDate"),
54
+ EndDate: stringField(payload, "endDate"),
55
+ HasGroups: boolField(payload, "hasGroups"),
56
+ HasStandings: boolField(payload, "hasStandings"),
57
+ GroupsRef: refFromField(payload, "groups"),
58
+ Extensions: extensionsFromMap(payload,
59
+ "$ref", "id", "name", "displayName", "abbreviation", "startDate", "endDate", "hasGroups", "hasStandings", "groups", "season",
60
+ ),
61
+ }, nil
62
+ }
63
+
64
+ // NormalizeSeasonGroup maps a season-group payload into the normalized season group shape.
65
+ func NormalizeSeasonGroup(data []byte) (*SeasonGroup, error) {
66
+ payload, err := decodePayloadMap(data)
67
+ if err != nil {
68
+ return nil, err
69
+ }
70
+
71
+ ref := stringField(payload, "$ref")
72
+ ids := refIDs(ref)
73
+ seasonRef := refFromField(payload, "season")
74
+ seasonIDs := refIDs(seasonRef)
75
+ standingsRef := refFromField(payload, "standings")
76
+ standingsIDs := refIDs(standingsRef)
77
+
78
+ return &SeasonGroup{
79
+ Ref: ref,
80
+ ID: nonEmpty(stringField(payload, "id"), ids["groupId"]),
81
+ LeagueID: nonEmpty(ids["leagueId"], seasonIDs["leagueId"], standingsIDs["leagueId"]),
82
+ SeasonID: nonEmpty(ids["seasonId"], seasonIDs["seasonId"], standingsIDs["seasonId"]),
83
+ TypeID: nonEmpty(ids["typeId"], standingsIDs["typeId"]),
84
+ Name: nonEmpty(stringField(payload, "name"), stringField(payload, "displayName")),
85
+ Abbreviation: stringField(payload, "abbreviation"),
86
+ StandingsRef: standingsRef,
87
+ Extensions: extensionsFromMap(payload,
88
+ "$ref", "id", "name", "displayName", "abbreviation", "season", "standings",
89
+ ),
90
+ }, nil
91
+ }
92
+
93
+ // NormalizeCalendarDays maps one section-shaped calendar payload into normalized calendar-day entries.
94
+ func NormalizeCalendarDays(data []byte) ([]CalendarDay, error) {
95
+ payload, err := decodePayloadMap(data)
96
+ if err != nil {
97
+ return nil, err
98
+ }
99
+
100
+ ref := stringField(payload, "$ref")
101
+ ids := refIDs(ref)
102
+ eventDate := mapField(payload, "eventDate")
103
+ dayType := nonEmpty(stringField(eventDate, "type"), strings.TrimPrefix(lastPathSegment(ref), "/"), stringField(payload, "type"))
104
+ startDate := stringField(payload, "startDate")
105
+ endDate := stringField(payload, "endDate")
106
+ sections := calendarSectionLabels(payload)
107
+ dates := stringSliceValueFromAny(eventDate["dates"])
108
+
109
+ if len(dates) == 0 {
110
+ dates = append(dates, nonEmpty(startDate, endDate))
111
+ }
112
+
113
+ items := make([]CalendarDay, 0, len(dates))
114
+ for _, date := range dates {
115
+ date = strings.TrimSpace(date)
116
+ if date == "" {
117
+ continue
118
+ }
119
+ items = append(items, CalendarDay{
120
+ Ref: ref,
121
+ LeagueID: ids["leagueId"],
122
+ Date: date,
123
+ DayType: dayType,
124
+ StartDate: startDate,
125
+ EndDate: endDate,
126
+ Sections: sections,
127
+ Extensions: extensionsFromMap(payload,
128
+ "$ref", "eventDate", "startDate", "endDate", "sections", "type",
129
+ ),
130
+ })
131
+ }
132
+
133
+ if len(items) == 0 {
134
+ return []CalendarDay{}, nil
135
+ }
136
+ return items, nil
137
+ }
138
+
139
+ // NormalizeStandingsGroup maps one standings payload into a normalized standings group.
140
+ func NormalizeStandingsGroup(data []byte) (*StandingsGroup, error) {
141
+ payload, err := decodePayloadMap(data)
142
+ if err != nil {
143
+ return nil, err
144
+ }
145
+ return NormalizeStandingsGroupFromMap(payload), nil
146
+ }
147
+
148
+ // NormalizeStandingsGroupFromMap maps a decoded standings payload into a normalized standings group.
149
+ func NormalizeStandingsGroupFromMap(payload map[string]any) *StandingsGroup {
150
+ ref := stringField(payload, "$ref")
151
+ ids := refIDs(ref)
152
+ entries := normalizeStandingsRows(payload)
153
+ standingsID := nonEmpty(stringField(payload, "id"), ids["standingsId"])
154
+
155
+ return &StandingsGroup{
156
+ Ref: ref,
157
+ ID: standingsID,
158
+ LeagueID: ids["leagueId"],
159
+ SeasonID: ids["seasonId"],
160
+ GroupID: ids["groupId"],
161
+ Entries: entries,
162
+ Extensions: extensionsFromMap(payload,
163
+ "$ref", "id", "name", "displayName", "standings", "entries",
164
+ ),
165
+ }
166
+ }
167
+
168
+ func normalizeStandingsRows(payload map[string]any) []Team {
169
+ rows := mapSliceField(payload, "standings")
170
+ if len(rows) == 0 {
171
+ rows = mapSliceField(payload, "entries")
172
+ }
173
+ if len(rows) == 0 {
174
+ return []Team{}
175
+ }
176
+
177
+ out := make([]Team, 0, len(rows))
178
+ for _, row := range rows {
179
+ out = append(out, normalizeStandingsTeamRow(row))
180
+ }
181
+ return out
182
+ }
183
+
184
+ func normalizeStandingsTeamRow(row map[string]any) Team {
185
+ teamMap := mapField(row, "team")
186
+ teamRef := stringField(teamMap, "$ref")
187
+ teamIDs := refIDs(teamRef)
188
+
189
+ team := Team{
190
+ Ref: teamRef,
191
+ ID: nonEmpty(stringField(teamMap, "id"), teamIDs["teamId"], teamIDs["competitorId"]),
192
+ Name: nonEmpty(stringField(teamMap, "displayName"), stringField(teamMap, "name")),
193
+ ShortName: nonEmpty(stringField(teamMap, "shortDisplayName"), stringField(teamMap, "shortName"), stringField(teamMap, "abbreviation")),
194
+ Abbreviation: stringField(teamMap, "abbreviation"),
195
+ Extensions: extensionsFromMap(row,
196
+ "team", "records", "record",
197
+ ),
198
+ }
199
+
200
+ records := standingsRecordRows(row)
201
+ rank := standingsStatValue(records, "rank")
202
+ points := standingsStatValue(records, "matchpoints", "points")
203
+ played := standingsStatValue(records, "matchesplayed")
204
+
205
+ parts := make([]string, 0, 3)
206
+ if rank != "" {
207
+ parts = append(parts, "Rank "+rank)
208
+ }
209
+ if points != "" {
210
+ parts = append(parts, "Pts "+points)
211
+ }
212
+ if played != "" {
213
+ parts = append(parts, "P "+played)
214
+ }
215
+ team.ScoreSummary = strings.Join(parts, " | ")
216
+
217
+ if team.Extensions == nil {
218
+ team.Extensions = map[string]any{}
219
+ }
220
+ if len(records) > 0 {
221
+ team.Extensions["records"] = records
222
+ }
223
+
224
+ return team
225
+ }
226
+
227
+ func standingsRecordRows(row map[string]any) []map[string]any {
228
+ records := mapSliceField(row, "records")
229
+ if len(records) > 0 {
230
+ return records
231
+ }
232
+ record := mapField(row, "record")
233
+ if len(record) == 0 {
234
+ return nil
235
+ }
236
+ return []map[string]any{record}
237
+ }
238
+
239
+ func standingsStatValue(records []map[string]any, names ...string) string {
240
+ if len(records) == 0 || len(names) == 0 {
241
+ return ""
242
+ }
243
+
244
+ targets := map[string]struct{}{}
245
+ for _, name := range names {
246
+ key := strings.ToLower(strings.TrimSpace(name))
247
+ if key != "" {
248
+ targets[key] = struct{}{}
249
+ }
250
+ }
251
+
252
+ for _, record := range records {
253
+ for _, stat := range mapSliceField(record, "stats") {
254
+ name := strings.ToLower(strings.TrimSpace(stringField(stat, "name")))
255
+ typ := strings.ToLower(strings.TrimSpace(stringField(stat, "type")))
256
+ if _, ok := targets[name]; !ok {
257
+ if _, ok = targets[typ]; !ok {
258
+ continue
259
+ }
260
+ }
261
+
262
+ value := nonEmpty(stringField(stat, "displayValue"), stringField(stat, "value"))
263
+ if value != "" {
264
+ return value
265
+ }
266
+ }
267
+ }
268
+
269
+ return ""
270
+ }
271
+
272
+ func calendarSectionLabels(payload map[string]any) []string {
273
+ sections := mapSliceField(payload, "sections")
274
+ if len(sections) == 0 {
275
+ return nil
276
+ }
277
+
278
+ out := make([]string, 0, len(sections))
279
+ seen := map[string]struct{}{}
280
+ for _, section := range sections {
281
+ label := nonEmpty(stringField(section, "label"), stringField(section, "name"), stringField(section, "displayName"))
282
+ label = strings.TrimSpace(label)
283
+ if label == "" {
284
+ continue
285
+ }
286
+ if _, ok := seen[label]; ok {
287
+ continue
288
+ }
289
+ seen[label] = struct{}{}
290
+ out = append(out, label)
291
+ }
292
+ return out
293
+ }
294
+
295
+ func stringSliceValueFromAny(value any) []string {
296
+ raw, ok := value.([]any)
297
+ if !ok {
298
+ return nil
299
+ }
300
+ out := make([]string, 0, len(raw))
301
+ for _, entry := range raw {
302
+ asString, ok := entry.(string)
303
+ if !ok {
304
+ continue
305
+ }
306
+ asString = strings.TrimSpace(asString)
307
+ if asString == "" {
308
+ continue
309
+ }
310
+ out = append(out, asString)
311
+ }
312
+ return out
313
+ }
314
+
315
+ func firstNonZeroValue(values ...int) int {
316
+ for _, value := range values {
317
+ if value != 0 {
318
+ return value
319
+ }
320
+ }
321
+ return 0
322
+ }
323
+
324
+ func lastPathSegment(ref string) string {
325
+ ref = strings.TrimSpace(ref)
326
+ if ref == "" {
327
+ return ""
328
+ }
329
+ ids := refIDs(ref)
330
+ if len(ids) == 0 {
331
+ return ""
332
+ }
333
+ parts := strings.Split(strings.Trim(ref, "/"), "/")
334
+ if len(parts) == 0 {
335
+ return ""
336
+ }
337
+ return parts[len(parts)-1]
338
+ }
339
+
340
+ func encodePayloadMap(payload map[string]any) []byte {
341
+ data, err := json.Marshal(payload)
342
+ if err != nil {
343
+ return []byte("{}")
344
+ }
345
+ return data
346
+ }