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,1727 @@
|
|
|
1
|
+
package cricinfo
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"fmt"
|
|
6
|
+
"strconv"
|
|
7
|
+
"strings"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// NormalizeMatch maps a competition payload into the normalized match shape.
|
|
11
|
+
func NormalizeMatch(data []byte) (*Match, error) {
|
|
12
|
+
payload, err := decodePayloadMap(data)
|
|
13
|
+
if err != nil {
|
|
14
|
+
return nil, err
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
match := normalizeMatchFromCompetitionMap(payload, eventMatchContext{})
|
|
18
|
+
return &match, nil
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// NormalizeMatchesFromEvent maps an event payload into one or more normalized match entries.
|
|
22
|
+
func NormalizeMatchesFromEvent(data []byte) ([]Match, error) {
|
|
23
|
+
payload, err := decodePayloadMap(data)
|
|
24
|
+
if err != nil {
|
|
25
|
+
return nil, err
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
context := buildEventMatchContext(payload)
|
|
29
|
+
competitions := mapSliceField(payload, "competitions")
|
|
30
|
+
if len(competitions) == 0 {
|
|
31
|
+
return []Match{}, nil
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
matches := make([]Match, 0, len(competitions))
|
|
35
|
+
for _, competition := range competitions {
|
|
36
|
+
matches = append(matches, normalizeMatchFromCompetitionMap(competition, context))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return matches, nil
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// NormalizePlayer maps an athlete profile payload into the normalized player shape.
|
|
43
|
+
func NormalizePlayer(data []byte) (*Player, error) {
|
|
44
|
+
payload, err := decodePayloadMap(data)
|
|
45
|
+
if err != nil {
|
|
46
|
+
return nil, err
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
ref := stringField(payload, "$ref")
|
|
50
|
+
ids := refIDs(ref)
|
|
51
|
+
position := mapField(payload, "position")
|
|
52
|
+
styles := normalizePlayerStyles(payload)
|
|
53
|
+
majorTeams := normalizePlayerAffiliations(mapSliceField(payload, "majorTeams"))
|
|
54
|
+
debuts := normalizePlayerDebuts(mapSliceField(payload, "debuts"))
|
|
55
|
+
team := normalizePlayerAffiliation(mapField(payload, "team"))
|
|
56
|
+
|
|
57
|
+
player := &Player{
|
|
58
|
+
Ref: ref,
|
|
59
|
+
ID: nonEmpty(stringField(payload, "id"), ids["athleteId"]),
|
|
60
|
+
UID: stringField(payload, "uid"),
|
|
61
|
+
GUID: stringField(payload, "guid"),
|
|
62
|
+
Type: stringField(payload, "type"),
|
|
63
|
+
Name: stringField(payload, "name"),
|
|
64
|
+
FirstName: stringField(payload, "firstName"),
|
|
65
|
+
MiddleName: stringField(payload, "middleName"),
|
|
66
|
+
LastName: stringField(payload, "lastName"),
|
|
67
|
+
DisplayName: stringField(payload, "displayName"),
|
|
68
|
+
FullName: stringField(payload, "fullName"),
|
|
69
|
+
ShortName: stringField(payload, "shortName"),
|
|
70
|
+
BattingName: stringField(payload, "battingName"),
|
|
71
|
+
FieldingName: stringField(payload, "fieldingName"),
|
|
72
|
+
Gender: stringField(payload, "gender"),
|
|
73
|
+
Age: intField(payload, "age"),
|
|
74
|
+
DateOfBirth: stringField(payload, "dateOfBirth"),
|
|
75
|
+
DateOfBirthDisplay: nonEmpty(stringField(payload, "dateOfBirthStr"), stringField(payload, "dateOfBirth")),
|
|
76
|
+
Active: truthyField(payload, "active") || truthyField(payload, "isActive"),
|
|
77
|
+
Position: stringField(position, "name"),
|
|
78
|
+
PositionRef: stringField(position, "$ref"),
|
|
79
|
+
PositionAbbreviation: stringField(position, "abbreviation"),
|
|
80
|
+
Styles: styles,
|
|
81
|
+
Team: team,
|
|
82
|
+
MajorTeams: majorTeams,
|
|
83
|
+
Debuts: debuts,
|
|
84
|
+
NewsRef: refFromField(payload, "news"),
|
|
85
|
+
Extensions: extensionsFromMap(payload,
|
|
86
|
+
"$ref", "id", "uid", "guid", "type", "name", "firstName", "middleName", "lastName",
|
|
87
|
+
"displayName", "fullName", "shortName", "battingName", "fieldingName", "gender", "age",
|
|
88
|
+
"dateOfBirth", "dateOfBirthStr", "active", "isActive", "team", "position", "style", "styles",
|
|
89
|
+
"majorTeams", "debuts", "news",
|
|
90
|
+
),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return player, nil
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// NormalizePlayerStatistics maps an athlete statistics payload into a grouped split/category view.
|
|
97
|
+
func NormalizePlayerStatistics(data []byte) (*PlayerStatistics, error) {
|
|
98
|
+
payload, err := decodePayloadMap(data)
|
|
99
|
+
if err != nil {
|
|
100
|
+
return nil, err
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
ref := stringField(payload, "$ref")
|
|
104
|
+
ids := refIDs(ref)
|
|
105
|
+
athleteRef := refFromField(payload, "athlete")
|
|
106
|
+
splits := mapField(payload, "splits")
|
|
107
|
+
|
|
108
|
+
playerStats := &PlayerStatistics{
|
|
109
|
+
Ref: ref,
|
|
110
|
+
PlayerID: nonEmpty(refIDs(athleteRef)["athleteId"], ids["athleteId"]),
|
|
111
|
+
PlayerRef: athleteRef,
|
|
112
|
+
SplitID: stringField(splits, "id"),
|
|
113
|
+
Name: stringField(splits, "name"),
|
|
114
|
+
Abbreviation: stringField(splits, "abbreviation"),
|
|
115
|
+
Categories: []StatCategory{},
|
|
116
|
+
Extensions: extensionsFromMap(payload,
|
|
117
|
+
"$ref", "athlete", "competition", "team", "splits",
|
|
118
|
+
),
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for _, item := range mapSliceField(splits, "categories") {
|
|
122
|
+
stats := make([]StatValue, 0, len(mapSliceField(item, "stats")))
|
|
123
|
+
for _, statRaw := range mapSliceField(item, "stats") {
|
|
124
|
+
stats = append(stats, StatValue{
|
|
125
|
+
Name: stringField(statRaw, "name"),
|
|
126
|
+
DisplayName: stringField(statRaw, "displayName"),
|
|
127
|
+
ShortName: stringField(statRaw, "shortDisplayName"),
|
|
128
|
+
Description: stringField(statRaw, "description"),
|
|
129
|
+
Abbreviation: stringField(statRaw, "abbreviation"),
|
|
130
|
+
DisplayValue: stringField(statRaw, "displayValue"),
|
|
131
|
+
Value: statRaw["value"],
|
|
132
|
+
Type: stringField(statRaw, "type"),
|
|
133
|
+
Extensions: extensionsFromMap(statRaw,
|
|
134
|
+
"name", "displayName", "shortDisplayName", "description", "abbreviation", "displayValue", "value", "type",
|
|
135
|
+
),
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
playerStats.Categories = append(playerStats.Categories, StatCategory{
|
|
140
|
+
Name: stringField(item, "name"),
|
|
141
|
+
DisplayName: nonEmpty(stringField(item, "displayName"), stringField(item, "name")),
|
|
142
|
+
ShortName: stringField(item, "shortDisplayName"),
|
|
143
|
+
Abbreviation: stringField(item, "abbreviation"),
|
|
144
|
+
Summary: stringField(item, "summary"),
|
|
145
|
+
Stats: stats,
|
|
146
|
+
Extensions: extensionsFromMap(item,
|
|
147
|
+
"name", "displayName", "shortDisplayName", "abbreviation", "summary", "stats",
|
|
148
|
+
),
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return playerStats, nil
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// NormalizeNewsArticle maps one Cricinfo news payload into a normalized article object.
|
|
156
|
+
func NormalizeNewsArticle(data []byte) (*NewsArticle, error) {
|
|
157
|
+
payload, err := decodePayloadMap(data)
|
|
158
|
+
if err != nil {
|
|
159
|
+
return nil, err
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
links := mapField(payload, "links")
|
|
163
|
+
web := mapField(links, "web")
|
|
164
|
+
api := mapField(mapField(links, "api"), "v1")
|
|
165
|
+
|
|
166
|
+
article := &NewsArticle{
|
|
167
|
+
Ref: stringField(payload, "$ref"),
|
|
168
|
+
ID: nonEmpty(stringField(payload, "id"), refIDs(stringField(payload, "$ref"))["newsId"]),
|
|
169
|
+
UID: stringField(payload, "uid"),
|
|
170
|
+
Type: stringField(payload, "type"),
|
|
171
|
+
Headline: stringField(payload, "headline"),
|
|
172
|
+
Title: stringField(payload, "title"),
|
|
173
|
+
LinkText: stringField(payload, "linkText"),
|
|
174
|
+
Byline: stringField(payload, "byline"),
|
|
175
|
+
Description: stringField(payload, "description"),
|
|
176
|
+
Published: stringField(payload, "published"),
|
|
177
|
+
LastModified: stringField(payload, "lastModified"),
|
|
178
|
+
WebURL: stringField(web, "href"),
|
|
179
|
+
APIURL: stringField(api, "href"),
|
|
180
|
+
Extensions: extensionsFromMap(payload,
|
|
181
|
+
"$ref", "id", "uid", "type", "headline", "title", "linkText", "byline", "description",
|
|
182
|
+
"published", "lastModified", "links",
|
|
183
|
+
),
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return article, nil
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// NormalizeTeam maps a competitor/team payload into the normalized team shape.
|
|
190
|
+
func NormalizeTeam(data []byte) (*Team, error) {
|
|
191
|
+
payload, err := decodePayloadMap(data)
|
|
192
|
+
if err != nil {
|
|
193
|
+
return nil, err
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
team := normalizeTeamMap(payload)
|
|
197
|
+
return &team, nil
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// NormalizeTeamRosterEntries maps match-scoped competitor roster payloads into roster entries.
|
|
201
|
+
func NormalizeTeamRosterEntries(data []byte, team Team, scope TeamScope, matchID string) ([]TeamRosterEntry, error) {
|
|
202
|
+
entries, err := DecodeObjectCollection[map[string]any](data, "entries")
|
|
203
|
+
if err != nil {
|
|
204
|
+
return nil, err
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
normalized := make([]TeamRosterEntry, 0, len(entries))
|
|
208
|
+
for _, entry := range entries {
|
|
209
|
+
athlete := mapField(entry, "athlete")
|
|
210
|
+
athleteRef := refFromField(entry, "athlete")
|
|
211
|
+
playerID := nonEmpty(stringField(entry, "playerId"), refIDs(athleteRef)["athleteId"], refIDs(stringField(entry, "$ref"))["athleteId"])
|
|
212
|
+
displayName := nonEmpty(
|
|
213
|
+
stringField(athlete, "displayName"),
|
|
214
|
+
stringField(athlete, "fullName"),
|
|
215
|
+
stringField(athlete, "name"),
|
|
216
|
+
stringField(entry, "displayName"),
|
|
217
|
+
stringField(entry, "fullName"),
|
|
218
|
+
stringField(entry, "name"),
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
normalized = append(normalized, TeamRosterEntry{
|
|
222
|
+
PlayerID: playerID,
|
|
223
|
+
PlayerRef: athleteRef,
|
|
224
|
+
DisplayName: displayName,
|
|
225
|
+
TeamID: team.ID,
|
|
226
|
+
TeamRef: team.Ref,
|
|
227
|
+
MatchID: strings.TrimSpace(matchID),
|
|
228
|
+
Scope: scope,
|
|
229
|
+
Captain: boolField(entry, "captain"),
|
|
230
|
+
Starter: boolField(entry, "starter"),
|
|
231
|
+
Active: boolField(entry, "active"),
|
|
232
|
+
ActiveName: stringField(entry, "activeName"),
|
|
233
|
+
PositionRef: refFromField(entry, "position"),
|
|
234
|
+
LinescoresRef: refFromField(entry, "linescores"),
|
|
235
|
+
StatisticsRef: refFromField(entry, "statistics"),
|
|
236
|
+
Extensions: extensionsFromMap(entry,
|
|
237
|
+
"$ref", "playerId", "athlete", "captain", "starter", "active", "activeName", "position", "linescores", "statistics",
|
|
238
|
+
),
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return normalized, nil
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// NormalizeTeamAthletePage maps a global team athletes page into roster-like entries for player-command bridging.
|
|
246
|
+
func NormalizeTeamAthletePage(data []byte, team Team) ([]TeamRosterEntry, error) {
|
|
247
|
+
page, err := DecodePage[Ref](data)
|
|
248
|
+
if err != nil {
|
|
249
|
+
return nil, err
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
entries := make([]TeamRosterEntry, 0, len(page.Items))
|
|
253
|
+
for _, item := range page.Items {
|
|
254
|
+
playerRef := strings.TrimSpace(item.URL)
|
|
255
|
+
if playerRef == "" {
|
|
256
|
+
continue
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
entries = append(entries, TeamRosterEntry{
|
|
260
|
+
PlayerID: refIDs(playerRef)["athleteId"],
|
|
261
|
+
PlayerRef: playerRef,
|
|
262
|
+
TeamID: team.ID,
|
|
263
|
+
TeamRef: team.Ref,
|
|
264
|
+
Scope: TeamScopeGlobal,
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return entries, nil
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// NormalizeTeamScore maps a score payload into a stable team score object.
|
|
272
|
+
func NormalizeTeamScore(data []byte, team Team, scope TeamScope, matchID string) (*TeamScore, error) {
|
|
273
|
+
payload, err := decodePayloadMap(data)
|
|
274
|
+
if err != nil {
|
|
275
|
+
return nil, err
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
score := &TeamScore{
|
|
279
|
+
Ref: nonEmpty(stringField(payload, "$ref"), team.ScoreRef),
|
|
280
|
+
TeamID: team.ID,
|
|
281
|
+
MatchID: strings.TrimSpace(matchID),
|
|
282
|
+
Scope: scope,
|
|
283
|
+
DisplayValue: stringField(payload, "displayValue"),
|
|
284
|
+
Value: stringField(payload, "value"),
|
|
285
|
+
Place: stringField(payload, "place"),
|
|
286
|
+
Source: stringField(payload, "source"),
|
|
287
|
+
Winner: boolField(payload, "winner"),
|
|
288
|
+
Extensions: extensionsFromMap(payload,
|
|
289
|
+
"$ref", "displayValue", "value", "place", "source", "winner",
|
|
290
|
+
),
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return score, nil
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// NormalizeTeamLeaders maps category-based team leaders payloads into batting/bowling-friendly structures.
|
|
297
|
+
func NormalizeTeamLeaders(data []byte, team Team, scope TeamScope, matchID string) (*TeamLeaders, error) {
|
|
298
|
+
payload, err := decodePayloadMap(data)
|
|
299
|
+
if err != nil {
|
|
300
|
+
return nil, err
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
leaders := &TeamLeaders{
|
|
304
|
+
Ref: stringField(payload, "$ref"),
|
|
305
|
+
TeamID: team.ID,
|
|
306
|
+
TeamName: nonEmpty(team.ShortName, team.Name),
|
|
307
|
+
MatchID: strings.TrimSpace(matchID),
|
|
308
|
+
Scope: scope,
|
|
309
|
+
Name: nonEmpty(stringField(payload, "displayName"), stringField(payload, "name"), nonEmpty(team.ShortName, team.Name), "Leaders"),
|
|
310
|
+
Categories: []TeamLeaderCategory{},
|
|
311
|
+
Extensions: extensionsFromMap(payload, "$ref", "id", "name", "displayName", "abbreviation", "categories"),
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
for _, rawCategory := range mapSliceField(payload, "categories") {
|
|
315
|
+
category := TeamLeaderCategory{
|
|
316
|
+
Name: stringField(rawCategory, "name"),
|
|
317
|
+
DisplayName: nonEmpty(stringField(rawCategory, "displayName"), stringField(rawCategory, "name")),
|
|
318
|
+
ShortName: stringField(rawCategory, "shortDisplayName"),
|
|
319
|
+
Abbreviation: stringField(rawCategory, "abbreviation"),
|
|
320
|
+
Leaders: []TeamLeaderEntry{},
|
|
321
|
+
Extensions: extensionsFromMap(rawCategory,
|
|
322
|
+
"name", "displayName", "shortDisplayName", "abbreviation", "leaders",
|
|
323
|
+
),
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
for _, rawLeader := range mapSliceField(rawCategory, "leaders") {
|
|
327
|
+
athlete := mapField(rawLeader, "athlete")
|
|
328
|
+
athleteRef := refFromField(rawLeader, "athlete")
|
|
329
|
+
athleteID := nonEmpty(
|
|
330
|
+
stringField(rawLeader, "athleteId"),
|
|
331
|
+
stringField(athlete, "id"),
|
|
332
|
+
refIDs(athleteRef)["athleteId"],
|
|
333
|
+
)
|
|
334
|
+
athleteName := nonEmpty(
|
|
335
|
+
stringField(athlete, "displayName"),
|
|
336
|
+
stringField(athlete, "fullName"),
|
|
337
|
+
stringField(athlete, "name"),
|
|
338
|
+
stringField(rawLeader, "athleteDisplayName"),
|
|
339
|
+
stringField(rawLeader, "name"),
|
|
340
|
+
)
|
|
341
|
+
entry := TeamLeaderEntry{
|
|
342
|
+
Order: intField(rawLeader, "order"),
|
|
343
|
+
DisplayValue: stringField(rawLeader, "displayValue"),
|
|
344
|
+
Value: stringField(rawLeader, "value"),
|
|
345
|
+
AthleteID: athleteID,
|
|
346
|
+
AthleteName: athleteName,
|
|
347
|
+
AthleteRef: athleteRef,
|
|
348
|
+
TeamRef: refFromField(rawLeader, "team"),
|
|
349
|
+
StatisticsRef: refFromField(rawLeader, "statistics"),
|
|
350
|
+
Runs: stringField(rawLeader, "runs"),
|
|
351
|
+
Wickets: stringField(rawLeader, "wickets"),
|
|
352
|
+
Overs: stringField(rawLeader, "overs"),
|
|
353
|
+
Maidens: stringField(rawLeader, "maidens"),
|
|
354
|
+
EconomyRate: stringField(rawLeader, "economyRate"),
|
|
355
|
+
Balls: stringField(rawLeader, "balls"),
|
|
356
|
+
Fours: stringField(rawLeader, "fours"),
|
|
357
|
+
Sixes: stringField(rawLeader, "sixes"),
|
|
358
|
+
Extensions: extensionsFromMap(rawLeader,
|
|
359
|
+
"order", "displayValue", "value", "athlete", "team", "statistics", "runs", "wickets", "overs", "maidens", "economyRate", "balls", "fours", "sixes",
|
|
360
|
+
),
|
|
361
|
+
}
|
|
362
|
+
category.Leaders = append(category.Leaders, entry)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
leaders.Categories = append(leaders.Categories, category)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return leaders, nil
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// NormalizeTeamRecordCategories maps team records pages into stat-category-like entries.
|
|
372
|
+
func NormalizeTeamRecordCategories(data []byte) ([]StatCategory, error) {
|
|
373
|
+
payload, err := decodePayloadMap(data)
|
|
374
|
+
if err != nil {
|
|
375
|
+
return nil, err
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
items := mapSliceField(payload, "items")
|
|
379
|
+
if len(items) == 0 {
|
|
380
|
+
return []StatCategory{}, nil
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
categories := make([]StatCategory, 0, len(items))
|
|
384
|
+
for _, item := range items {
|
|
385
|
+
stats := make([]StatValue, 0)
|
|
386
|
+
for _, statRaw := range mapSliceField(item, "stats") {
|
|
387
|
+
stats = append(stats, StatValue{
|
|
388
|
+
Name: stringField(statRaw, "name"),
|
|
389
|
+
DisplayName: stringField(statRaw, "displayName"),
|
|
390
|
+
ShortName: stringField(statRaw, "shortDisplayName"),
|
|
391
|
+
Description: stringField(statRaw, "description"),
|
|
392
|
+
Abbreviation: stringField(statRaw, "abbreviation"),
|
|
393
|
+
DisplayValue: stringField(statRaw, "displayValue"),
|
|
394
|
+
Value: statRaw["value"],
|
|
395
|
+
Type: stringField(statRaw, "type"),
|
|
396
|
+
Extensions: extensionsFromMap(statRaw,
|
|
397
|
+
"name", "displayName", "shortDisplayName", "description", "abbreviation", "displayValue", "value", "type",
|
|
398
|
+
),
|
|
399
|
+
})
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
categories = append(categories, StatCategory{
|
|
403
|
+
Name: stringField(item, "name"),
|
|
404
|
+
DisplayName: nonEmpty(stringField(item, "displayName"), stringField(item, "name")),
|
|
405
|
+
ShortName: stringField(item, "shortDisplayName"),
|
|
406
|
+
Abbreviation: stringField(item, "abbreviation"),
|
|
407
|
+
Summary: stringField(item, "summary"),
|
|
408
|
+
Stats: stats,
|
|
409
|
+
Extensions: extensionsFromMap(item,
|
|
410
|
+
"$ref", "id", "name", "displayName", "shortDisplayName", "abbreviation", "summary", "stats",
|
|
411
|
+
),
|
|
412
|
+
})
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return categories, nil
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// NormalizeLeague maps a league/root payload into the normalized league shape.
|
|
419
|
+
func NormalizeLeague(data []byte) (*League, error) {
|
|
420
|
+
payload, err := decodePayloadMap(data)
|
|
421
|
+
if err != nil {
|
|
422
|
+
return nil, err
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
ref := stringField(payload, "$ref")
|
|
426
|
+
ids := refIDs(ref)
|
|
427
|
+
|
|
428
|
+
league := &League{
|
|
429
|
+
Ref: ref,
|
|
430
|
+
ID: nonEmpty(stringField(payload, "id"), ids["leagueId"]),
|
|
431
|
+
UID: stringField(payload, "uid"),
|
|
432
|
+
Name: stringField(payload, "name"),
|
|
433
|
+
Slug: stringField(payload, "slug"),
|
|
434
|
+
SeasonRef: refFromField(payload, "season"),
|
|
435
|
+
Extensions: extensionsFromMap(payload,
|
|
436
|
+
"$ref", "id", "uid", "name", "slug", "season",
|
|
437
|
+
),
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return league, nil
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// NormalizeSeasonList maps a seasons page payload into normalized season entries.
|
|
444
|
+
func NormalizeSeasonList(data []byte) ([]Season, error) {
|
|
445
|
+
payload, err := decodePayloadMap(data)
|
|
446
|
+
if err != nil {
|
|
447
|
+
return nil, err
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
items := mapSliceField(payload, "items")
|
|
451
|
+
seasons := make([]Season, 0, len(items))
|
|
452
|
+
for _, item := range items {
|
|
453
|
+
ref := stringField(item, "$ref")
|
|
454
|
+
ids := refIDs(ref)
|
|
455
|
+
season := Season{
|
|
456
|
+
Ref: ref,
|
|
457
|
+
ID: ids["seasonId"],
|
|
458
|
+
LeagueID: ids["leagueId"],
|
|
459
|
+
Year: parseYear(ids["seasonId"]),
|
|
460
|
+
Extensions: extensionsFromMap(item,
|
|
461
|
+
"$ref",
|
|
462
|
+
),
|
|
463
|
+
}
|
|
464
|
+
seasons = append(seasons, season)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return seasons, nil
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// NormalizeStandingsGroups maps standings payloads into normalized group entries.
|
|
471
|
+
func NormalizeStandingsGroups(data []byte) ([]StandingsGroup, error) {
|
|
472
|
+
payload, err := decodePayloadMap(data)
|
|
473
|
+
if err != nil {
|
|
474
|
+
return nil, err
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
items := mapSliceField(payload, "items")
|
|
478
|
+
groups := make([]StandingsGroup, 0, len(items))
|
|
479
|
+
for _, item := range items {
|
|
480
|
+
ref := stringField(item, "$ref")
|
|
481
|
+
ids := refIDs(ref)
|
|
482
|
+
group := StandingsGroup{
|
|
483
|
+
Ref: ref,
|
|
484
|
+
ID: ids["standingsId"],
|
|
485
|
+
LeagueID: ids["leagueId"],
|
|
486
|
+
SeasonID: ids["seasonId"],
|
|
487
|
+
GroupID: ids["groupId"],
|
|
488
|
+
Entries: normalizeStandingsEntries(item),
|
|
489
|
+
Extensions: extensionsFromMap(item,
|
|
490
|
+
"$ref", "entries",
|
|
491
|
+
),
|
|
492
|
+
}
|
|
493
|
+
groups = append(groups, group)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return groups, nil
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// NormalizeInnings maps an innings payload into the normalized innings shape.
|
|
500
|
+
func NormalizeInnings(data []byte) (*Innings, error) {
|
|
501
|
+
payload, err := decodePayloadMap(data)
|
|
502
|
+
if err != nil {
|
|
503
|
+
return nil, err
|
|
504
|
+
}
|
|
505
|
+
return normalizeInningsFromMap(payload), nil
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
func normalizeInningsFromMap(payload map[string]any) *Innings {
|
|
509
|
+
ref := stringField(payload, "$ref")
|
|
510
|
+
ids := refIDs(ref)
|
|
511
|
+
inningsNumber := parseInt(ids["inningsId"])
|
|
512
|
+
period := intField(payload, "period")
|
|
513
|
+
if period == 0 {
|
|
514
|
+
period = parseInt(ids["periodId"])
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
innings := &Innings{
|
|
518
|
+
Ref: ref,
|
|
519
|
+
ID: ids["inningsId"],
|
|
520
|
+
LeagueID: ids["leagueId"],
|
|
521
|
+
EventID: ids["eventId"],
|
|
522
|
+
CompetitionID: ids["competitionId"],
|
|
523
|
+
MatchID: ids["competitionId"],
|
|
524
|
+
TeamID: ids["competitorId"],
|
|
525
|
+
InningsNumber: inningsNumber,
|
|
526
|
+
Period: period,
|
|
527
|
+
Runs: intField(payload, "runs"),
|
|
528
|
+
Wickets: intField(payload, "wickets"),
|
|
529
|
+
Overs: floatField(payload, "overs"),
|
|
530
|
+
Score: stringField(payload, "score"),
|
|
531
|
+
Description: stringField(payload, "description"),
|
|
532
|
+
Target: intField(payload, "target"),
|
|
533
|
+
IsBatting: truthyField(payload, "isBatting"),
|
|
534
|
+
IsCurrent: truthyField(payload, "isCurrent"),
|
|
535
|
+
Fours: intField(payload, "fours"),
|
|
536
|
+
Sixes: intField(payload, "sixes"),
|
|
537
|
+
StatisticsRef: refFromField(payload, "statistics"),
|
|
538
|
+
LeadersRef: refFromField(payload, "leaders"),
|
|
539
|
+
PartnershipsRef: refFromField(payload, "partnerships"),
|
|
540
|
+
FallOfWicketRef: refFromField(payload, "fow"),
|
|
541
|
+
Extensions: extensionsFromMap(payload,
|
|
542
|
+
"$ref", "period", "runs", "wickets", "overs", "score", "description", "target",
|
|
543
|
+
"isBatting", "isCurrent", "fours", "sixes", "value", "displayValue", "source", "followOn",
|
|
544
|
+
"statistics", "leaders", "partnerships", "fow",
|
|
545
|
+
),
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return innings
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// NormalizeInningsPeriodStatistics maps period statistics payloads into over and wicket timelines.
|
|
552
|
+
func NormalizeInningsPeriodStatistics(data []byte) ([]InningsOver, []InningsWicket, error) {
|
|
553
|
+
payload, err := decodePayloadMap(data)
|
|
554
|
+
if err != nil {
|
|
555
|
+
return nil, nil, err
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
splits := mapField(payload, "splits")
|
|
559
|
+
if splits == nil {
|
|
560
|
+
return []InningsOver{}, []InningsWicket{}, nil
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
overs := normalizeOverTimeline(splits)
|
|
564
|
+
wickets := make([]InningsWicket, 0)
|
|
565
|
+
for _, over := range overs {
|
|
566
|
+
wickets = append(wickets, over.Wickets...)
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return overs, wickets, nil
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
func normalizeOverTimeline(splits map[string]any) []InningsOver {
|
|
573
|
+
if splits == nil {
|
|
574
|
+
return []InningsOver{}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
rawOvers, ok := splits["overs"]
|
|
578
|
+
if !ok || rawOvers == nil {
|
|
579
|
+
return []InningsOver{}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
rows := make([]map[string]any, 0)
|
|
583
|
+
switch typed := rawOvers.(type) {
|
|
584
|
+
case []any:
|
|
585
|
+
for _, entry := range typed {
|
|
586
|
+
switch rowOrSlice := entry.(type) {
|
|
587
|
+
case map[string]any:
|
|
588
|
+
rows = append(rows, rowOrSlice)
|
|
589
|
+
case []any:
|
|
590
|
+
for _, nested := range rowOrSlice {
|
|
591
|
+
row, ok := nested.(map[string]any)
|
|
592
|
+
if !ok {
|
|
593
|
+
continue
|
|
594
|
+
}
|
|
595
|
+
rows = append(rows, row)
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
overs := make([]InningsOver, 0, len(rows))
|
|
602
|
+
for _, row := range rows {
|
|
603
|
+
wickets := normalizeTimelineWickets(row)
|
|
604
|
+
overs = append(overs, InningsOver{
|
|
605
|
+
Number: intField(row, "number"),
|
|
606
|
+
Runs: intField(row, "runs"),
|
|
607
|
+
WicketCount: len(wickets),
|
|
608
|
+
Wickets: wickets,
|
|
609
|
+
Extensions: extensionsFromMap(row, "number", "runs", "wicket"),
|
|
610
|
+
})
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return overs
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
func normalizeTimelineWickets(overRow map[string]any) []InningsWicket {
|
|
617
|
+
if overRow == nil {
|
|
618
|
+
return []InningsWicket{}
|
|
619
|
+
}
|
|
620
|
+
rawWickets, ok := overRow["wicket"]
|
|
621
|
+
if !ok || rawWickets == nil {
|
|
622
|
+
return []InningsWicket{}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
entries, ok := rawWickets.([]any)
|
|
626
|
+
if !ok {
|
|
627
|
+
return []InningsWicket{}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
wickets := make([]InningsWicket, 0, len(entries))
|
|
631
|
+
for _, raw := range entries {
|
|
632
|
+
entry, ok := raw.(map[string]any)
|
|
633
|
+
if !ok {
|
|
634
|
+
continue
|
|
635
|
+
}
|
|
636
|
+
details := mapField(entry, "details")
|
|
637
|
+
wickets = append(wickets, InningsWicket{
|
|
638
|
+
Number: intField(entry, "number"),
|
|
639
|
+
FOW: stringField(entry, "fow"),
|
|
640
|
+
Over: stringField(entry, "over"),
|
|
641
|
+
FOWType: stringField(entry, "fowType"),
|
|
642
|
+
Runs: intField(entry, "runs"),
|
|
643
|
+
BallsFaced: intField(entry, "ballsFaced"),
|
|
644
|
+
StrikeRate: floatField(entry, "strikeRate"),
|
|
645
|
+
DismissalCard: stringField(entry, "dismissalCard"),
|
|
646
|
+
ShortText: stringField(entry, "shortText"),
|
|
647
|
+
DetailRef: stringField(details, "$ref"),
|
|
648
|
+
DetailShortText: stringField(details, "shortText"),
|
|
649
|
+
DetailText: stringField(details, "text"),
|
|
650
|
+
Extensions: extensionsFromMap(entry,
|
|
651
|
+
"number", "fow", "over", "fowType", "runs", "ballsFaced", "dismissalCard", "shortText", "details",
|
|
652
|
+
),
|
|
653
|
+
})
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return wickets
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// NormalizeDeliveryEvent maps a detail payload into the normalized delivery-event shape.
|
|
660
|
+
func NormalizeDeliveryEvent(data []byte) (*DeliveryEvent, error) {
|
|
661
|
+
payload, err := decodePayloadMap(data)
|
|
662
|
+
if err != nil {
|
|
663
|
+
return nil, err
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
ref := stringField(payload, "$ref")
|
|
667
|
+
ids := refIDs(ref)
|
|
668
|
+
over := mapField(payload, "over")
|
|
669
|
+
playType := mapField(payload, "playType")
|
|
670
|
+
dismissal := mapField(payload, "dismissal")
|
|
671
|
+
batsman := mapField(payload, "batsman")
|
|
672
|
+
bowler := mapField(payload, "bowler")
|
|
673
|
+
fielder := mapField(dismissal, "fielder")
|
|
674
|
+
batsmanRef := nonEmpty(nestedRef(payload, "batsman", "athlete"), refFromField(payload, "batsman"))
|
|
675
|
+
bowlerRef := nonEmpty(nestedRef(payload, "bowler", "athlete"), refFromField(payload, "bowler"))
|
|
676
|
+
fielderRef := nonEmpty(nestedRef(payload, "dismissal", "fielder", "athlete"), nestedRef(payload, "dismissal", "fielder"))
|
|
677
|
+
batsmanID := nonEmpty(stringField(batsman, "playerId"), stringField(batsman, "id"), refIDs(batsmanRef)["athleteId"])
|
|
678
|
+
bowlerID := nonEmpty(stringField(bowler, "playerId"), stringField(bowler, "id"), refIDs(bowlerRef)["athleteId"])
|
|
679
|
+
fielderID := nonEmpty(stringField(fielder, "playerId"), stringField(fielder, "id"), refIDs(fielderRef)["athleteId"])
|
|
680
|
+
teamRef := refFromField(payload, "team")
|
|
681
|
+
teamIDs := refIDs(teamRef)
|
|
682
|
+
athletePlayerIDs := extractAthletePlayerIDs(payload)
|
|
683
|
+
xCoordinate := nullableFloatField(payload, "xCoordinate")
|
|
684
|
+
yCoordinate := nullableFloatField(payload, "yCoordinate")
|
|
685
|
+
bbbTimestamp := int64Field(payload, "bbbTimestamp")
|
|
686
|
+
|
|
687
|
+
event := &DeliveryEvent{
|
|
688
|
+
Ref: ref,
|
|
689
|
+
ID: nonEmpty(stringField(payload, "id"), ids["detailId"]),
|
|
690
|
+
LeagueID: ids["leagueId"],
|
|
691
|
+
EventID: ids["eventId"],
|
|
692
|
+
CompetitionID: ids["competitionId"],
|
|
693
|
+
MatchID: ids["competitionId"],
|
|
694
|
+
TeamID: nonEmpty(teamIDs["teamId"], teamIDs["competitorId"]),
|
|
695
|
+
Period: intField(payload, "period"),
|
|
696
|
+
PeriodText: stringField(payload, "periodText"),
|
|
697
|
+
OverNumber: intField(over, "number"),
|
|
698
|
+
BallNumber: intField(over, "ball"),
|
|
699
|
+
ScoreValue: intField(payload, "scoreValue"),
|
|
700
|
+
ShortText: stringField(payload, "shortText"),
|
|
701
|
+
Text: stringField(payload, "text"),
|
|
702
|
+
HomeScore: stringField(payload, "homeScore"),
|
|
703
|
+
AwayScore: stringField(payload, "awayScore"),
|
|
704
|
+
BatsmanRef: batsmanRef,
|
|
705
|
+
BowlerRef: bowlerRef,
|
|
706
|
+
BatsmanPlayerID: batsmanID,
|
|
707
|
+
BowlerPlayerID: bowlerID,
|
|
708
|
+
FielderPlayerID: fielderID,
|
|
709
|
+
AthletePlayerIDs: athletePlayerIDs,
|
|
710
|
+
PlayType: playType,
|
|
711
|
+
Dismissal: dismissal,
|
|
712
|
+
DismissalType: stringField(dismissal, "type"),
|
|
713
|
+
DismissalName: nonEmpty(stringField(dismissal, "name"), stringField(dismissal, "type")),
|
|
714
|
+
DismissalCard: stringField(dismissal, "dismissalCard"),
|
|
715
|
+
DismissalText: stringField(dismissal, "text"),
|
|
716
|
+
SpeedKPH: floatField(payload, "speedKPH"),
|
|
717
|
+
XCoordinate: xCoordinate,
|
|
718
|
+
YCoordinate: yCoordinate,
|
|
719
|
+
BBBTimestamp: bbbTimestamp,
|
|
720
|
+
CoordinateX: xCoordinate,
|
|
721
|
+
CoordinateY: yCoordinate,
|
|
722
|
+
Timestamp: bbbTimestamp,
|
|
723
|
+
Extensions: extensionsFromMap(payload,
|
|
724
|
+
"$ref", "id", "period", "periodText", "over", "scoreValue", "shortText", "text", "homeScore", "awayScore",
|
|
725
|
+
"batsman", "bowler", "playType", "dismissal", "speedKPH", "xCoordinate", "yCoordinate", "bbbTimestamp",
|
|
726
|
+
),
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return event, nil
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
func extractAthletePlayerIDs(payload map[string]any) []string {
|
|
733
|
+
if payload == nil {
|
|
734
|
+
return nil
|
|
735
|
+
}
|
|
736
|
+
rawItems, ok := payload["athletesInvolved"].([]any)
|
|
737
|
+
if !ok || len(rawItems) == 0 {
|
|
738
|
+
return nil
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
seen := map[string]struct{}{}
|
|
742
|
+
out := make([]string, 0, len(rawItems))
|
|
743
|
+
for _, raw := range rawItems {
|
|
744
|
+
ref := refValue(raw)
|
|
745
|
+
if ref == "" {
|
|
746
|
+
if mapped, ok := raw.(map[string]any); ok {
|
|
747
|
+
ref = nonEmpty(refFromField(mapped, "athlete"), nestedRef(mapped, "athlete"))
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
playerID := strings.TrimSpace(refIDs(ref)["athleteId"])
|
|
751
|
+
if playerID == "" {
|
|
752
|
+
continue
|
|
753
|
+
}
|
|
754
|
+
if _, ok := seen[playerID]; ok {
|
|
755
|
+
continue
|
|
756
|
+
}
|
|
757
|
+
seen[playerID] = struct{}{}
|
|
758
|
+
out = append(out, playerID)
|
|
759
|
+
}
|
|
760
|
+
if len(out) == 0 {
|
|
761
|
+
return nil
|
|
762
|
+
}
|
|
763
|
+
return out
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// NormalizeMatchScorecard maps a matchcards payload into batting, bowling, and partnerships views.
|
|
767
|
+
func NormalizeMatchScorecard(data []byte, match Match) (*MatchScorecard, error) {
|
|
768
|
+
payload, err := decodePayloadMap(data)
|
|
769
|
+
if err != nil {
|
|
770
|
+
return nil, err
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
scorecard := &MatchScorecard{
|
|
774
|
+
Ref: nonEmpty(stringField(payload, "$ref"), matchSubresourceRef(match, "matchcards", "matchcards")),
|
|
775
|
+
LeagueID: match.LeagueID,
|
|
776
|
+
EventID: match.EventID,
|
|
777
|
+
CompetitionID: match.CompetitionID,
|
|
778
|
+
MatchID: match.ID,
|
|
779
|
+
BattingCards: []BattingCard{},
|
|
780
|
+
BowlingCards: []BowlingCard{},
|
|
781
|
+
PartnershipCards: []PartnershipCard{},
|
|
782
|
+
Extensions: extensionsFromMap(payload,
|
|
783
|
+
"$ref", "count", "items", "pageCount", "pageIndex", "pageSize",
|
|
784
|
+
),
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
for _, item := range mapSliceField(payload, "items") {
|
|
788
|
+
headline := strings.ToLower(strings.TrimSpace(stringField(item, "headline")))
|
|
789
|
+
typeID := strings.TrimSpace(stringField(item, "typeID"))
|
|
790
|
+
|
|
791
|
+
switch {
|
|
792
|
+
case headline == "batting" || typeID == "11":
|
|
793
|
+
scorecard.BattingCards = append(scorecard.BattingCards, normalizeBattingCard(item))
|
|
794
|
+
case headline == "bowling" || typeID == "12":
|
|
795
|
+
scorecard.BowlingCards = append(scorecard.BowlingCards, normalizeBowlingCard(item))
|
|
796
|
+
case headline == "partnerships" || typeID == "13":
|
|
797
|
+
scorecard.PartnershipCards = append(scorecard.PartnershipCards, normalizePartnershipCard(item))
|
|
798
|
+
default:
|
|
799
|
+
// Preserve unclassified card payloads for --all-fields without failing command execution.
|
|
800
|
+
if scorecard.Extensions == nil {
|
|
801
|
+
scorecard.Extensions = map[string]any{}
|
|
802
|
+
}
|
|
803
|
+
unknown, _ := scorecard.Extensions["unknownCards"].([]any)
|
|
804
|
+
scorecard.Extensions["unknownCards"] = append(unknown, item)
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
return scorecard, nil
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// NormalizeMatchSituation maps a situation payload into a stable shape that tolerates sparse data.
|
|
812
|
+
func NormalizeMatchSituation(data []byte, match Match) (*MatchSituation, error) {
|
|
813
|
+
payload, err := decodePayloadMap(data)
|
|
814
|
+
if err != nil {
|
|
815
|
+
return nil, err
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
situation := &MatchSituation{
|
|
819
|
+
Ref: nonEmpty(stringField(payload, "$ref"), matchSubresourceRef(match, "situation", "situation")),
|
|
820
|
+
LeagueID: match.LeagueID,
|
|
821
|
+
EventID: match.EventID,
|
|
822
|
+
CompetitionID: match.CompetitionID,
|
|
823
|
+
MatchID: match.ID,
|
|
824
|
+
OddsRef: refFromField(payload, "odds"),
|
|
825
|
+
Data: extensionsFromMap(payload, "$ref", "odds"),
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
return situation, nil
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
func normalizeBattingCard(payload map[string]any) BattingCard {
|
|
832
|
+
card := BattingCard{
|
|
833
|
+
InningsNumber: parseInt(stringField(payload, "inningsNumber")),
|
|
834
|
+
TeamName: stringField(payload, "teamName"),
|
|
835
|
+
Runs: stringField(payload, "runs"),
|
|
836
|
+
Total: stringField(payload, "total"),
|
|
837
|
+
Extras: stringField(payload, "extras"),
|
|
838
|
+
Players: []BattingCardEntry{},
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
for _, row := range mapSliceField(payload, "playerDetails") {
|
|
842
|
+
card.Players = append(card.Players, BattingCardEntry{
|
|
843
|
+
PlayerID: stringField(row, "playerID"),
|
|
844
|
+
PlayerName: stringField(row, "playerName"),
|
|
845
|
+
Dismissal: stringField(row, "dismissal"),
|
|
846
|
+
Runs: stringField(row, "runs"),
|
|
847
|
+
BallsFaced: stringField(row, "ballsFaced"),
|
|
848
|
+
Fours: stringField(row, "fours"),
|
|
849
|
+
Sixes: stringField(row, "sixes"),
|
|
850
|
+
Href: stringField(row, "href"),
|
|
851
|
+
})
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
return card
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
func normalizeBowlingCard(payload map[string]any) BowlingCard {
|
|
858
|
+
card := BowlingCard{
|
|
859
|
+
InningsNumber: parseInt(stringField(payload, "inningsNumber")),
|
|
860
|
+
TeamName: stringField(payload, "teamName"),
|
|
861
|
+
Players: []BowlingCardEntry{},
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
for _, row := range mapSliceField(payload, "playerDetails") {
|
|
865
|
+
card.Players = append(card.Players, BowlingCardEntry{
|
|
866
|
+
PlayerID: stringField(row, "playerID"),
|
|
867
|
+
PlayerName: stringField(row, "playerName"),
|
|
868
|
+
Overs: stringField(row, "overs"),
|
|
869
|
+
Maidens: stringField(row, "maidens"),
|
|
870
|
+
Conceded: stringField(row, "conceded"),
|
|
871
|
+
Wickets: stringField(row, "wickets"),
|
|
872
|
+
EconomyRate: stringField(row, "economyRate"),
|
|
873
|
+
NBW: stringField(row, "nbw"),
|
|
874
|
+
Href: stringField(row, "href"),
|
|
875
|
+
})
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
return card
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
func normalizePartnershipCard(payload map[string]any) PartnershipCard {
|
|
882
|
+
card := PartnershipCard{
|
|
883
|
+
InningsNumber: parseInt(stringField(payload, "inningsNumber")),
|
|
884
|
+
TeamName: stringField(payload, "teamName"),
|
|
885
|
+
Players: []PartnershipCardEntry{},
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
for _, row := range mapSliceField(payload, "playerDetails") {
|
|
889
|
+
card.Players = append(card.Players, PartnershipCardEntry{
|
|
890
|
+
PartnershipRuns: stringField(row, "partnershipRuns"),
|
|
891
|
+
PartnershipOvers: stringField(row, "partnershipOvers"),
|
|
892
|
+
PartnershipWicketName: stringField(row, "partnershipWicketName"),
|
|
893
|
+
FOWType: stringField(row, "fowType"),
|
|
894
|
+
Player1Name: stringField(row, "player1Name"),
|
|
895
|
+
Player1Runs: stringField(row, "player1Runs"),
|
|
896
|
+
Player2Name: stringField(row, "player2Name"),
|
|
897
|
+
Player2Runs: stringField(row, "player2Runs"),
|
|
898
|
+
})
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
return card
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// NormalizeStatCategories maps a stats payload into normalized category entries.
|
|
905
|
+
func NormalizeStatCategories(data []byte) ([]StatCategory, error) {
|
|
906
|
+
payload, err := decodePayloadMap(data)
|
|
907
|
+
if err != nil {
|
|
908
|
+
return nil, err
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
splits := mapField(payload, "splits")
|
|
912
|
+
if splits == nil {
|
|
913
|
+
return []StatCategory{}, nil
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
categories := make([]StatCategory, 0)
|
|
917
|
+
for _, item := range mapSliceField(splits, "categories") {
|
|
918
|
+
stats := make([]StatValue, 0)
|
|
919
|
+
for _, statRaw := range mapSliceField(item, "stats") {
|
|
920
|
+
stats = append(stats, StatValue{
|
|
921
|
+
Name: stringField(statRaw, "name"),
|
|
922
|
+
DisplayName: stringField(statRaw, "displayName"),
|
|
923
|
+
ShortName: stringField(statRaw, "shortDisplayName"),
|
|
924
|
+
Description: stringField(statRaw, "description"),
|
|
925
|
+
Abbreviation: stringField(statRaw, "abbreviation"),
|
|
926
|
+
DisplayValue: stringField(statRaw, "displayValue"),
|
|
927
|
+
Value: statRaw["value"],
|
|
928
|
+
Type: stringField(statRaw, "type"),
|
|
929
|
+
Extensions: extensionsFromMap(statRaw,
|
|
930
|
+
"name", "displayName", "shortDisplayName", "description", "abbreviation", "displayValue", "value", "type",
|
|
931
|
+
),
|
|
932
|
+
})
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
categories = append(categories, StatCategory{
|
|
936
|
+
Name: stringField(item, "name"),
|
|
937
|
+
DisplayName: stringField(item, "displayName"),
|
|
938
|
+
ShortName: stringField(item, "shortDisplayName"),
|
|
939
|
+
Abbreviation: stringField(item, "abbreviation"),
|
|
940
|
+
Summary: stringField(item, "summary"),
|
|
941
|
+
Stats: stats,
|
|
942
|
+
Extensions: extensionsFromMap(item,
|
|
943
|
+
"name", "displayName", "shortDisplayName", "abbreviation", "summary", "stats",
|
|
944
|
+
),
|
|
945
|
+
})
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
return categories, nil
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// NormalizePartnerships maps a partnerships page into normalized partnership entries.
|
|
952
|
+
func NormalizePartnerships(data []byte) ([]Partnership, error) {
|
|
953
|
+
payload, err := decodePayloadMap(data)
|
|
954
|
+
if err != nil {
|
|
955
|
+
return nil, err
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
items := mapSliceField(payload, "items")
|
|
959
|
+
partnerships := make([]Partnership, 0, len(items))
|
|
960
|
+
for _, item := range items {
|
|
961
|
+
ref := stringField(item, "$ref")
|
|
962
|
+
ids := refIDs(ref)
|
|
963
|
+
partnerships = append(partnerships, Partnership{
|
|
964
|
+
Ref: ref,
|
|
965
|
+
ID: ids["partnershipId"],
|
|
966
|
+
TeamID: ids["competitorId"],
|
|
967
|
+
MatchID: ids["competitionId"],
|
|
968
|
+
InningsID: ids["inningsId"],
|
|
969
|
+
Period: ids["periodId"],
|
|
970
|
+
Order: parseInt(ids["partnershipId"]),
|
|
971
|
+
WicketNumber: parseInt(ids["partnershipId"]),
|
|
972
|
+
Extensions: extensionsFromMap(item,
|
|
973
|
+
"$ref",
|
|
974
|
+
),
|
|
975
|
+
})
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
return partnerships, nil
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// NormalizePartnership maps a single partnership payload into a detailed normalized object.
|
|
982
|
+
func NormalizePartnership(data []byte) (*Partnership, error) {
|
|
983
|
+
payload, err := decodePayloadMap(data)
|
|
984
|
+
if err != nil {
|
|
985
|
+
return nil, err
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
ref := stringField(payload, "$ref")
|
|
989
|
+
ids := refIDs(ref)
|
|
990
|
+
start := mapField(payload, "start")
|
|
991
|
+
end := mapField(payload, "end")
|
|
992
|
+
|
|
993
|
+
batsmen := make([]PartnershipBatsman, 0)
|
|
994
|
+
for _, batsman := range mapSliceField(payload, "batsmen") {
|
|
995
|
+
athleteRef := strings.TrimSpace(stringField(batsman, "athlete"))
|
|
996
|
+
if athleteRef == "" {
|
|
997
|
+
athleteRef = refFromField(batsman, "athlete")
|
|
998
|
+
}
|
|
999
|
+
batsmen = append(batsmen, PartnershipBatsman{
|
|
1000
|
+
AthleteRef: athleteRef,
|
|
1001
|
+
Balls: intField(batsman, "balls"),
|
|
1002
|
+
Runs: intField(batsman, "runs"),
|
|
1003
|
+
})
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
partnership := &Partnership{
|
|
1007
|
+
Ref: ref,
|
|
1008
|
+
ID: ids["partnershipId"],
|
|
1009
|
+
TeamID: ids["competitorId"],
|
|
1010
|
+
MatchID: ids["competitionId"],
|
|
1011
|
+
InningsID: ids["inningsId"],
|
|
1012
|
+
Period: ids["periodId"],
|
|
1013
|
+
Order: parseInt(ids["partnershipId"]),
|
|
1014
|
+
WicketNumber: intField(payload, "wicketNumber"),
|
|
1015
|
+
WicketName: stringField(payload, "wicketName"),
|
|
1016
|
+
FOWType: stringField(payload, "fowType"),
|
|
1017
|
+
Overs: floatField(payload, "overs"),
|
|
1018
|
+
Runs: intField(payload, "runs"),
|
|
1019
|
+
RunRate: floatField(payload, "runRate"),
|
|
1020
|
+
Start: PartnershipSnapshot{
|
|
1021
|
+
Overs: floatField(start, "overs"),
|
|
1022
|
+
Runs: intField(start, "runs"),
|
|
1023
|
+
Wickets: intField(start, "wickets"),
|
|
1024
|
+
},
|
|
1025
|
+
End: PartnershipSnapshot{
|
|
1026
|
+
Overs: floatField(end, "overs"),
|
|
1027
|
+
Runs: intField(end, "runs"),
|
|
1028
|
+
Wickets: intField(end, "wickets"),
|
|
1029
|
+
},
|
|
1030
|
+
Batsmen: batsmen,
|
|
1031
|
+
Extensions: extensionsFromMap(payload,
|
|
1032
|
+
"$ref", "wicketNumber", "wicketName", "fowType", "overs", "runs", "runRate", "start", "end", "batsmen",
|
|
1033
|
+
),
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
return partnership, nil
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// NormalizeFallOfWickets maps a fall-of-wicket page into normalized entries.
|
|
1040
|
+
func NormalizeFallOfWickets(data []byte) ([]FallOfWicket, error) {
|
|
1041
|
+
payload, err := decodePayloadMap(data)
|
|
1042
|
+
if err != nil {
|
|
1043
|
+
return nil, err
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
items := mapSliceField(payload, "items")
|
|
1047
|
+
wickets := make([]FallOfWicket, 0, len(items))
|
|
1048
|
+
for _, item := range items {
|
|
1049
|
+
ref := stringField(item, "$ref")
|
|
1050
|
+
ids := refIDs(ref)
|
|
1051
|
+
wickets = append(wickets, FallOfWicket{
|
|
1052
|
+
Ref: ref,
|
|
1053
|
+
ID: ids["fowId"],
|
|
1054
|
+
MatchID: ids["competitionId"],
|
|
1055
|
+
TeamID: ids["competitorId"],
|
|
1056
|
+
InningsID: ids["inningsId"],
|
|
1057
|
+
Period: ids["periodId"],
|
|
1058
|
+
WicketNumber: parseInt(ids["fowId"]),
|
|
1059
|
+
Extensions: extensionsFromMap(item,
|
|
1060
|
+
"$ref",
|
|
1061
|
+
),
|
|
1062
|
+
})
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
return wickets, nil
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// NormalizeFallOfWicket maps a single fall-of-wicket payload into a detailed normalized object.
|
|
1069
|
+
func NormalizeFallOfWicket(data []byte) (*FallOfWicket, error) {
|
|
1070
|
+
payload, err := decodePayloadMap(data)
|
|
1071
|
+
if err != nil {
|
|
1072
|
+
return nil, err
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
ref := stringField(payload, "$ref")
|
|
1076
|
+
ids := refIDs(ref)
|
|
1077
|
+
|
|
1078
|
+
fow := &FallOfWicket{
|
|
1079
|
+
Ref: ref,
|
|
1080
|
+
ID: ids["fowId"],
|
|
1081
|
+
MatchID: ids["competitionId"],
|
|
1082
|
+
TeamID: ids["competitorId"],
|
|
1083
|
+
InningsID: ids["inningsId"],
|
|
1084
|
+
Period: ids["periodId"],
|
|
1085
|
+
WicketNumber: intField(payload, "wicketNumber"),
|
|
1086
|
+
WicketOver: floatField(payload, "wicketOver"),
|
|
1087
|
+
FOWType: stringField(payload, "fowType"),
|
|
1088
|
+
Runs: intField(payload, "runs"),
|
|
1089
|
+
RunsScored: intField(payload, "runsScored"),
|
|
1090
|
+
BallsFaced: intField(payload, "ballsFaced"),
|
|
1091
|
+
AthleteRef: refFromField(payload, "athlete"),
|
|
1092
|
+
Extensions: extensionsFromMap(payload,
|
|
1093
|
+
"$ref", "wicketNumber", "wicketOver", "fowType", "runs", "runsScored", "ballsFaced", "athlete",
|
|
1094
|
+
),
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
if fow.WicketNumber == 0 {
|
|
1098
|
+
fow.WicketNumber = parseInt(ids["fowId"])
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
return fow, nil
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
func normalizeStandingsEntries(item map[string]any) []Team {
|
|
1105
|
+
entries := make([]Team, 0)
|
|
1106
|
+
for _, raw := range mapSliceField(item, "entries") {
|
|
1107
|
+
entries = append(entries, normalizeTeamMap(raw))
|
|
1108
|
+
}
|
|
1109
|
+
return entries
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
func normalizeTeamMap(payload map[string]any) Team {
|
|
1113
|
+
ref := stringField(payload, "$ref")
|
|
1114
|
+
teamRef := refFromField(payload, "team")
|
|
1115
|
+
ids := refIDs(ref)
|
|
1116
|
+
|
|
1117
|
+
name := stringField(payload, "displayName")
|
|
1118
|
+
if name == "" {
|
|
1119
|
+
name = stringField(payload, "name")
|
|
1120
|
+
}
|
|
1121
|
+
shortName := stringField(payload, "shortName")
|
|
1122
|
+
if shortName == "" {
|
|
1123
|
+
shortName = stringField(payload, "shortDisplayName")
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
id := nonEmpty(stringField(payload, "id"), ids["teamId"], ids["competitorId"])
|
|
1127
|
+
score := mapField(payload, "score")
|
|
1128
|
+
scoreSummary := nonEmpty(stringField(score, "displayValue"), stringField(score, "value"))
|
|
1129
|
+
|
|
1130
|
+
return Team{
|
|
1131
|
+
Ref: nonEmpty(teamRef, ref),
|
|
1132
|
+
ID: id,
|
|
1133
|
+
UID: stringField(payload, "uid"),
|
|
1134
|
+
Name: name,
|
|
1135
|
+
ShortName: shortName,
|
|
1136
|
+
Abbreviation: stringField(payload, "abbreviation"),
|
|
1137
|
+
ScoreSummary: scoreSummary,
|
|
1138
|
+
Type: stringField(payload, "type"),
|
|
1139
|
+
HomeAway: stringField(payload, "homeAway"),
|
|
1140
|
+
Order: intField(payload, "order"),
|
|
1141
|
+
Winner: boolField(payload, "winner"),
|
|
1142
|
+
ScoreRef: refFromField(payload, "score"),
|
|
1143
|
+
RosterRef: refFromField(payload, "roster"),
|
|
1144
|
+
LeadersRef: refFromField(payload, "leaders"),
|
|
1145
|
+
StatisticsRef: refFromField(payload, "statistics"),
|
|
1146
|
+
RecordRef: nonEmpty(refFromField(payload, "record"), refFromField(payload, "records")),
|
|
1147
|
+
LinescoresRef: refFromField(payload, "linescores"),
|
|
1148
|
+
Extensions: extensionsFromMap(payload,
|
|
1149
|
+
"$ref", "id", "uid", "displayName", "name", "shortName", "shortDisplayName", "abbreviation",
|
|
1150
|
+
"type", "homeAway", "order", "winner", "score", "roster", "leaders", "statistics", "record", "records", "linescores",
|
|
1151
|
+
),
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
type eventMatchContext struct {
|
|
1156
|
+
ref string
|
|
1157
|
+
leagueID string
|
|
1158
|
+
eventID string
|
|
1159
|
+
date string
|
|
1160
|
+
endDate string
|
|
1161
|
+
description string
|
|
1162
|
+
shortDescription string
|
|
1163
|
+
venueName string
|
|
1164
|
+
venueSummary string
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
func buildEventMatchContext(payload map[string]any) eventMatchContext {
|
|
1168
|
+
ref := stringField(payload, "$ref")
|
|
1169
|
+
ids := refIDs(ref)
|
|
1170
|
+
leagueID := ids["leagueId"]
|
|
1171
|
+
|
|
1172
|
+
if leagueID == "" {
|
|
1173
|
+
for _, league := range mapSliceField(payload, "leagues") {
|
|
1174
|
+
leagueID = nonEmpty(
|
|
1175
|
+
stringField(league, "id"),
|
|
1176
|
+
refIDs(stringField(league, "$ref"))["leagueId"],
|
|
1177
|
+
)
|
|
1178
|
+
if leagueID != "" {
|
|
1179
|
+
break
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
venueName, venueSummary := eventVenueSummary(payload)
|
|
1185
|
+
return eventMatchContext{
|
|
1186
|
+
ref: ref,
|
|
1187
|
+
leagueID: leagueID,
|
|
1188
|
+
eventID: nonEmpty(stringField(payload, "id"), ids["eventId"]),
|
|
1189
|
+
date: stringField(payload, "date"),
|
|
1190
|
+
endDate: stringField(payload, "endDate"),
|
|
1191
|
+
description: nonEmpty(stringField(payload, "description"), stringField(payload, "name")),
|
|
1192
|
+
shortDescription: nonEmpty(stringField(payload, "shortDescription"), stringField(payload, "shortName")),
|
|
1193
|
+
venueName: venueName,
|
|
1194
|
+
venueSummary: venueSummary,
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
func normalizeMatchFromCompetitionMap(payload map[string]any, context eventMatchContext) Match {
|
|
1199
|
+
ref := stringField(payload, "$ref")
|
|
1200
|
+
ids := refIDs(ref)
|
|
1201
|
+
|
|
1202
|
+
venue := mapField(payload, "venue")
|
|
1203
|
+
venueName := nonEmpty(stringField(venue, "fullName"), context.venueName)
|
|
1204
|
+
venueSummary := nonEmpty(venueAddressSummary(venue), context.venueSummary)
|
|
1205
|
+
|
|
1206
|
+
teams := make([]Team, 0)
|
|
1207
|
+
for _, item := range mapSliceField(payload, "competitors") {
|
|
1208
|
+
teams = append(teams, normalizeTeamMap(item))
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
scoreSummary := matchScoreSummary(teams)
|
|
1212
|
+
matchState := nonEmpty(
|
|
1213
|
+
stringField(payload, "state"),
|
|
1214
|
+
stringField(payload, "summary"),
|
|
1215
|
+
stringField(payload, "statusSummary"),
|
|
1216
|
+
stringField(mapField(payload, "status"), "summary"),
|
|
1217
|
+
stringField(mapField(mapField(payload, "status"), "type"), "detail"),
|
|
1218
|
+
stringField(mapField(mapField(payload, "status"), "type"), "description"),
|
|
1219
|
+
)
|
|
1220
|
+
|
|
1221
|
+
return Match{
|
|
1222
|
+
Ref: nonEmpty(ref, context.ref),
|
|
1223
|
+
ID: nonEmpty(stringField(payload, "id"), ids["competitionId"]),
|
|
1224
|
+
UID: stringField(payload, "uid"),
|
|
1225
|
+
LeagueID: nonEmpty(ids["leagueId"], context.leagueID),
|
|
1226
|
+
EventID: nonEmpty(ids["eventId"], context.eventID),
|
|
1227
|
+
CompetitionID: nonEmpty(ids["competitionId"], stringField(payload, "id")),
|
|
1228
|
+
Description: nonEmpty(stringField(payload, "description"), context.description),
|
|
1229
|
+
ShortDescription: nonEmpty(stringField(payload, "shortDescription"), context.shortDescription),
|
|
1230
|
+
Note: stringField(payload, "note"),
|
|
1231
|
+
MatchState: matchState,
|
|
1232
|
+
Date: nonEmpty(stringField(payload, "date"), context.date),
|
|
1233
|
+
EndDate: nonEmpty(stringField(payload, "endDate"), context.endDate),
|
|
1234
|
+
VenueName: venueName,
|
|
1235
|
+
VenueSummary: venueSummary,
|
|
1236
|
+
ScoreSummary: scoreSummary,
|
|
1237
|
+
StatusRef: refFromField(payload, "status"),
|
|
1238
|
+
DetailsRef: refFromField(payload, "details"),
|
|
1239
|
+
Teams: teams,
|
|
1240
|
+
Extensions: extensionsFromMap(payload,
|
|
1241
|
+
"$ref", "id", "uid", "description", "shortDescription", "note", "state", "summary", "statusSummary",
|
|
1242
|
+
"date", "endDate", "status", "details", "competitors",
|
|
1243
|
+
),
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
func eventVenueSummary(payload map[string]any) (string, string) {
|
|
1248
|
+
venues := mapSliceField(payload, "venues")
|
|
1249
|
+
if len(venues) == 0 {
|
|
1250
|
+
return "", ""
|
|
1251
|
+
}
|
|
1252
|
+
return nonEmpty(
|
|
1253
|
+
stringField(venues[0], "fullName"),
|
|
1254
|
+
stringField(venues[0], "shortName"),
|
|
1255
|
+
), venueAddressSummary(venues[0])
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
func venueAddressSummary(venue map[string]any) string {
|
|
1259
|
+
if venue == nil {
|
|
1260
|
+
return ""
|
|
1261
|
+
}
|
|
1262
|
+
address := mapField(venue, "address")
|
|
1263
|
+
if address == nil {
|
|
1264
|
+
return ""
|
|
1265
|
+
}
|
|
1266
|
+
return nonEmpty(
|
|
1267
|
+
stringField(address, "summary"),
|
|
1268
|
+
strings.Join(compactValues(
|
|
1269
|
+
stringField(address, "city"),
|
|
1270
|
+
stringField(address, "state"),
|
|
1271
|
+
stringField(address, "country"),
|
|
1272
|
+
), ", "),
|
|
1273
|
+
)
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
func matchScoreSummary(teams []Team) string {
|
|
1277
|
+
parts := make([]string, 0, len(teams))
|
|
1278
|
+
for _, team := range teams {
|
|
1279
|
+
if team.ScoreSummary == "" {
|
|
1280
|
+
continue
|
|
1281
|
+
}
|
|
1282
|
+
label := nonEmpty(team.ShortName, team.Name, team.ID)
|
|
1283
|
+
if label == "" {
|
|
1284
|
+
parts = append(parts, team.ScoreSummary)
|
|
1285
|
+
continue
|
|
1286
|
+
}
|
|
1287
|
+
parts = append(parts, label+" "+team.ScoreSummary)
|
|
1288
|
+
}
|
|
1289
|
+
return strings.Join(parts, " | ")
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
func compactValues(values ...string) []string {
|
|
1293
|
+
out := make([]string, 0, len(values))
|
|
1294
|
+
for _, value := range values {
|
|
1295
|
+
value = strings.TrimSpace(value)
|
|
1296
|
+
if value == "" {
|
|
1297
|
+
continue
|
|
1298
|
+
}
|
|
1299
|
+
out = append(out, value)
|
|
1300
|
+
}
|
|
1301
|
+
return out
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
func decodePayloadMap(data []byte) (map[string]any, error) {
|
|
1305
|
+
var payload map[string]any
|
|
1306
|
+
if err := json.Unmarshal(data, &payload); err != nil {
|
|
1307
|
+
return nil, fmt.Errorf("decode payload: %w", err)
|
|
1308
|
+
}
|
|
1309
|
+
if payload == nil {
|
|
1310
|
+
return nil, fmt.Errorf("decode payload: empty object")
|
|
1311
|
+
}
|
|
1312
|
+
return payload, nil
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
func extensionsFromMap(payload map[string]any, knownKeys ...string) map[string]any {
|
|
1316
|
+
if len(payload) == 0 {
|
|
1317
|
+
return nil
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
known := map[string]struct{}{}
|
|
1321
|
+
for _, key := range knownKeys {
|
|
1322
|
+
known[key] = struct{}{}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
ext := map[string]any{}
|
|
1326
|
+
for key, value := range payload {
|
|
1327
|
+
if _, ok := known[key]; ok {
|
|
1328
|
+
continue
|
|
1329
|
+
}
|
|
1330
|
+
ext[key] = value
|
|
1331
|
+
}
|
|
1332
|
+
if len(ext) == 0 {
|
|
1333
|
+
return nil
|
|
1334
|
+
}
|
|
1335
|
+
return ext
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
func stringField(payload map[string]any, key string) string {
|
|
1339
|
+
if payload == nil {
|
|
1340
|
+
return ""
|
|
1341
|
+
}
|
|
1342
|
+
value, ok := payload[key]
|
|
1343
|
+
if !ok || value == nil {
|
|
1344
|
+
return ""
|
|
1345
|
+
}
|
|
1346
|
+
switch typed := value.(type) {
|
|
1347
|
+
case string:
|
|
1348
|
+
return strings.TrimSpace(typed)
|
|
1349
|
+
case float64:
|
|
1350
|
+
if typed == float64(int64(typed)) {
|
|
1351
|
+
return strconv.FormatInt(int64(typed), 10)
|
|
1352
|
+
}
|
|
1353
|
+
return strconv.FormatFloat(typed, 'f', -1, 64)
|
|
1354
|
+
case float32:
|
|
1355
|
+
if typed == float32(int64(typed)) {
|
|
1356
|
+
return strconv.FormatInt(int64(typed), 10)
|
|
1357
|
+
}
|
|
1358
|
+
return strconv.FormatFloat(float64(typed), 'f', -1, 32)
|
|
1359
|
+
case int:
|
|
1360
|
+
return strconv.Itoa(typed)
|
|
1361
|
+
case int64:
|
|
1362
|
+
return strconv.FormatInt(typed, 10)
|
|
1363
|
+
case json.Number:
|
|
1364
|
+
return strings.TrimSpace(typed.String())
|
|
1365
|
+
case fmt.Stringer:
|
|
1366
|
+
return strings.TrimSpace(typed.String())
|
|
1367
|
+
default:
|
|
1368
|
+
return strings.TrimSpace(fmt.Sprintf("%v", value))
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
func intField(payload map[string]any, key string) int {
|
|
1373
|
+
if payload == nil {
|
|
1374
|
+
return 0
|
|
1375
|
+
}
|
|
1376
|
+
value, ok := payload[key]
|
|
1377
|
+
if !ok || value == nil {
|
|
1378
|
+
return 0
|
|
1379
|
+
}
|
|
1380
|
+
switch typed := value.(type) {
|
|
1381
|
+
case float64:
|
|
1382
|
+
return int(typed)
|
|
1383
|
+
case float32:
|
|
1384
|
+
return int(typed)
|
|
1385
|
+
case int:
|
|
1386
|
+
return typed
|
|
1387
|
+
case int64:
|
|
1388
|
+
return int(typed)
|
|
1389
|
+
case json.Number:
|
|
1390
|
+
parsed, _ := typed.Int64()
|
|
1391
|
+
return int(parsed)
|
|
1392
|
+
case string:
|
|
1393
|
+
parsed, err := strconv.Atoi(strings.TrimSpace(typed))
|
|
1394
|
+
if err != nil {
|
|
1395
|
+
return 0
|
|
1396
|
+
}
|
|
1397
|
+
return parsed
|
|
1398
|
+
default:
|
|
1399
|
+
return 0
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
func int64Field(payload map[string]any, key string) int64 {
|
|
1404
|
+
if payload == nil {
|
|
1405
|
+
return 0
|
|
1406
|
+
}
|
|
1407
|
+
value, ok := payload[key]
|
|
1408
|
+
if !ok || value == nil {
|
|
1409
|
+
return 0
|
|
1410
|
+
}
|
|
1411
|
+
switch typed := value.(type) {
|
|
1412
|
+
case float64:
|
|
1413
|
+
return int64(typed)
|
|
1414
|
+
case float32:
|
|
1415
|
+
return int64(typed)
|
|
1416
|
+
case int:
|
|
1417
|
+
return int64(typed)
|
|
1418
|
+
case int64:
|
|
1419
|
+
return typed
|
|
1420
|
+
case json.Number:
|
|
1421
|
+
parsed, _ := typed.Int64()
|
|
1422
|
+
return parsed
|
|
1423
|
+
case string:
|
|
1424
|
+
parsed, err := strconv.ParseInt(strings.TrimSpace(typed), 10, 64)
|
|
1425
|
+
if err != nil {
|
|
1426
|
+
return 0
|
|
1427
|
+
}
|
|
1428
|
+
return parsed
|
|
1429
|
+
default:
|
|
1430
|
+
return 0
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
func floatField(payload map[string]any, key string) float64 {
|
|
1435
|
+
if payload == nil {
|
|
1436
|
+
return 0
|
|
1437
|
+
}
|
|
1438
|
+
value, ok := payload[key]
|
|
1439
|
+
if !ok || value == nil {
|
|
1440
|
+
return 0
|
|
1441
|
+
}
|
|
1442
|
+
switch typed := value.(type) {
|
|
1443
|
+
case float64:
|
|
1444
|
+
return typed
|
|
1445
|
+
case float32:
|
|
1446
|
+
return float64(typed)
|
|
1447
|
+
case int:
|
|
1448
|
+
return float64(typed)
|
|
1449
|
+
case int64:
|
|
1450
|
+
return float64(typed)
|
|
1451
|
+
case json.Number:
|
|
1452
|
+
parsed, _ := typed.Float64()
|
|
1453
|
+
return parsed
|
|
1454
|
+
case string:
|
|
1455
|
+
parsed, err := strconv.ParseFloat(strings.TrimSpace(typed), 64)
|
|
1456
|
+
if err != nil {
|
|
1457
|
+
return 0
|
|
1458
|
+
}
|
|
1459
|
+
return parsed
|
|
1460
|
+
default:
|
|
1461
|
+
return 0
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
func nullableFloatField(payload map[string]any, key string) *float64 {
|
|
1466
|
+
if payload == nil {
|
|
1467
|
+
return nil
|
|
1468
|
+
}
|
|
1469
|
+
value, ok := payload[key]
|
|
1470
|
+
if !ok || value == nil {
|
|
1471
|
+
return nil
|
|
1472
|
+
}
|
|
1473
|
+
parsed := floatField(payload, key)
|
|
1474
|
+
return &parsed
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
func boolField(payload map[string]any, key string) bool {
|
|
1478
|
+
if payload == nil {
|
|
1479
|
+
return false
|
|
1480
|
+
}
|
|
1481
|
+
value, ok := payload[key]
|
|
1482
|
+
if !ok || value == nil {
|
|
1483
|
+
return false
|
|
1484
|
+
}
|
|
1485
|
+
switch typed := value.(type) {
|
|
1486
|
+
case bool:
|
|
1487
|
+
return typed
|
|
1488
|
+
case string:
|
|
1489
|
+
parsed, err := strconv.ParseBool(strings.TrimSpace(typed))
|
|
1490
|
+
if err != nil {
|
|
1491
|
+
return false
|
|
1492
|
+
}
|
|
1493
|
+
return parsed
|
|
1494
|
+
default:
|
|
1495
|
+
return false
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
func truthyField(payload map[string]any, key string) bool {
|
|
1500
|
+
if boolField(payload, key) {
|
|
1501
|
+
return true
|
|
1502
|
+
}
|
|
1503
|
+
if payload == nil {
|
|
1504
|
+
return false
|
|
1505
|
+
}
|
|
1506
|
+
value, ok := payload[key]
|
|
1507
|
+
if !ok || value == nil {
|
|
1508
|
+
return false
|
|
1509
|
+
}
|
|
1510
|
+
switch typed := value.(type) {
|
|
1511
|
+
case float64:
|
|
1512
|
+
return typed != 0
|
|
1513
|
+
case float32:
|
|
1514
|
+
return typed != 0
|
|
1515
|
+
case int:
|
|
1516
|
+
return typed != 0
|
|
1517
|
+
case int64:
|
|
1518
|
+
return typed != 0
|
|
1519
|
+
case json.Number:
|
|
1520
|
+
parsed, err := typed.Int64()
|
|
1521
|
+
if err != nil {
|
|
1522
|
+
return false
|
|
1523
|
+
}
|
|
1524
|
+
return parsed != 0
|
|
1525
|
+
case string:
|
|
1526
|
+
trimmed := strings.TrimSpace(typed)
|
|
1527
|
+
if trimmed == "" {
|
|
1528
|
+
return false
|
|
1529
|
+
}
|
|
1530
|
+
if parsed, err := strconv.ParseInt(trimmed, 10, 64); err == nil {
|
|
1531
|
+
return parsed != 0
|
|
1532
|
+
}
|
|
1533
|
+
parsed, err := strconv.ParseBool(trimmed)
|
|
1534
|
+
if err != nil {
|
|
1535
|
+
return false
|
|
1536
|
+
}
|
|
1537
|
+
return parsed
|
|
1538
|
+
default:
|
|
1539
|
+
return false
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
func mapField(payload map[string]any, key string) map[string]any {
|
|
1544
|
+
if payload == nil {
|
|
1545
|
+
return nil
|
|
1546
|
+
}
|
|
1547
|
+
value, ok := payload[key]
|
|
1548
|
+
if !ok || value == nil {
|
|
1549
|
+
return nil
|
|
1550
|
+
}
|
|
1551
|
+
mapped, ok := value.(map[string]any)
|
|
1552
|
+
if !ok {
|
|
1553
|
+
return nil
|
|
1554
|
+
}
|
|
1555
|
+
return mapped
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
func mapSliceField(payload map[string]any, key string) []map[string]any {
|
|
1559
|
+
if payload == nil {
|
|
1560
|
+
return []map[string]any{}
|
|
1561
|
+
}
|
|
1562
|
+
value, ok := payload[key]
|
|
1563
|
+
if !ok || value == nil {
|
|
1564
|
+
return []map[string]any{}
|
|
1565
|
+
}
|
|
1566
|
+
rawItems, ok := value.([]any)
|
|
1567
|
+
if !ok {
|
|
1568
|
+
return []map[string]any{}
|
|
1569
|
+
}
|
|
1570
|
+
out := make([]map[string]any, 0, len(rawItems))
|
|
1571
|
+
for _, item := range rawItems {
|
|
1572
|
+
mapped, ok := item.(map[string]any)
|
|
1573
|
+
if !ok {
|
|
1574
|
+
continue
|
|
1575
|
+
}
|
|
1576
|
+
out = append(out, mapped)
|
|
1577
|
+
}
|
|
1578
|
+
return out
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
func refFromField(payload map[string]any, key string) string {
|
|
1582
|
+
if payload == nil {
|
|
1583
|
+
return ""
|
|
1584
|
+
}
|
|
1585
|
+
return refValue(payload[key])
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
func nestedRef(payload map[string]any, keys ...string) string {
|
|
1589
|
+
if len(keys) == 0 {
|
|
1590
|
+
return ""
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
var current any = payload
|
|
1594
|
+
for idx, key := range keys {
|
|
1595
|
+
mapped, ok := current.(map[string]any)
|
|
1596
|
+
if !ok || mapped == nil {
|
|
1597
|
+
return ""
|
|
1598
|
+
}
|
|
1599
|
+
next, ok := mapped[key]
|
|
1600
|
+
if !ok || next == nil {
|
|
1601
|
+
return ""
|
|
1602
|
+
}
|
|
1603
|
+
if idx == len(keys)-1 {
|
|
1604
|
+
return refValue(next)
|
|
1605
|
+
}
|
|
1606
|
+
current = next
|
|
1607
|
+
}
|
|
1608
|
+
return ""
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
func refValue(value any) string {
|
|
1612
|
+
switch typed := value.(type) {
|
|
1613
|
+
case string:
|
|
1614
|
+
return strings.TrimSpace(typed)
|
|
1615
|
+
case map[string]any:
|
|
1616
|
+
return strings.TrimSpace(stringField(typed, "$ref"))
|
|
1617
|
+
default:
|
|
1618
|
+
return ""
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
func normalizePlayerStyles(payload map[string]any) []PlayerStyle {
|
|
1623
|
+
rawStyles := append(mapSliceField(payload, "style"), mapSliceField(payload, "styles")...)
|
|
1624
|
+
if len(rawStyles) == 0 {
|
|
1625
|
+
return nil
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
out := make([]PlayerStyle, 0, len(rawStyles))
|
|
1629
|
+
seen := map[string]struct{}{}
|
|
1630
|
+
for _, raw := range rawStyles {
|
|
1631
|
+
style := PlayerStyle{
|
|
1632
|
+
Type: stringField(raw, "type"),
|
|
1633
|
+
Description: stringField(raw, "description"),
|
|
1634
|
+
ShortDescription: stringField(raw, "shortDescription"),
|
|
1635
|
+
}
|
|
1636
|
+
key := strings.Join([]string{style.Type, style.Description, style.ShortDescription}, "|")
|
|
1637
|
+
if _, ok := seen[key]; ok {
|
|
1638
|
+
continue
|
|
1639
|
+
}
|
|
1640
|
+
seen[key] = struct{}{}
|
|
1641
|
+
out = append(out, style)
|
|
1642
|
+
}
|
|
1643
|
+
return out
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
func normalizePlayerAffiliations(items []map[string]any) []PlayerAffiliation {
|
|
1647
|
+
if len(items) == 0 {
|
|
1648
|
+
return nil
|
|
1649
|
+
}
|
|
1650
|
+
out := make([]PlayerAffiliation, 0, len(items))
|
|
1651
|
+
for _, item := range items {
|
|
1652
|
+
if affiliation := normalizePlayerAffiliation(item); affiliation != nil {
|
|
1653
|
+
out = append(out, *affiliation)
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
return out
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
func normalizePlayerAffiliation(item map[string]any) *PlayerAffiliation {
|
|
1660
|
+
if len(item) == 0 {
|
|
1661
|
+
return nil
|
|
1662
|
+
}
|
|
1663
|
+
ref := stringField(item, "$ref")
|
|
1664
|
+
ids := refIDs(ref)
|
|
1665
|
+
return &PlayerAffiliation{
|
|
1666
|
+
ID: nonEmpty(stringField(item, "id"), ids["teamId"]),
|
|
1667
|
+
Ref: ref,
|
|
1668
|
+
Name: nonEmpty(stringField(item, "displayName"), stringField(item, "name"), stringField(item, "shortName")),
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
func normalizePlayerDebuts(items []map[string]any) []PlayerDebut {
|
|
1673
|
+
if len(items) == 0 {
|
|
1674
|
+
return nil
|
|
1675
|
+
}
|
|
1676
|
+
out := make([]PlayerDebut, 0, len(items))
|
|
1677
|
+
for _, item := range items {
|
|
1678
|
+
ref := stringField(item, "$ref")
|
|
1679
|
+
ids := refIDs(ref)
|
|
1680
|
+
out = append(out, PlayerDebut{
|
|
1681
|
+
ID: nonEmpty(stringField(item, "id"), ids["competitionId"], ids["eventId"]),
|
|
1682
|
+
Ref: ref,
|
|
1683
|
+
Name: nonEmpty(stringField(item, "displayName"), stringField(item, "name"), stringField(item, "shortName")),
|
|
1684
|
+
})
|
|
1685
|
+
}
|
|
1686
|
+
return out
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
func styleDescriptions(payload map[string]any, field string) []string {
|
|
1690
|
+
entries := mapSliceField(payload, field)
|
|
1691
|
+
out := make([]string, 0, len(entries))
|
|
1692
|
+
for _, entry := range entries {
|
|
1693
|
+
description := stringField(entry, "description")
|
|
1694
|
+
if description == "" {
|
|
1695
|
+
continue
|
|
1696
|
+
}
|
|
1697
|
+
out = append(out, description)
|
|
1698
|
+
}
|
|
1699
|
+
return out
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
func uniqueStrings(values []string) []string {
|
|
1703
|
+
seen := map[string]struct{}{}
|
|
1704
|
+
out := make([]string, 0, len(values))
|
|
1705
|
+
for _, value := range values {
|
|
1706
|
+
trimmed := strings.TrimSpace(value)
|
|
1707
|
+
if trimmed == "" {
|
|
1708
|
+
continue
|
|
1709
|
+
}
|
|
1710
|
+
if _, ok := seen[trimmed]; ok {
|
|
1711
|
+
continue
|
|
1712
|
+
}
|
|
1713
|
+
seen[trimmed] = struct{}{}
|
|
1714
|
+
out = append(out, trimmed)
|
|
1715
|
+
}
|
|
1716
|
+
return out
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
func nonEmpty(values ...string) string {
|
|
1720
|
+
for _, value := range values {
|
|
1721
|
+
trimmed := strings.TrimSpace(value)
|
|
1722
|
+
if trimmed != "" {
|
|
1723
|
+
return trimmed
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
return ""
|
|
1727
|
+
}
|