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,1689 @@
|
|
|
1
|
+
package cricinfo
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"fmt"
|
|
6
|
+
"io"
|
|
7
|
+
"regexp"
|
|
8
|
+
"sort"
|
|
9
|
+
"strings"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
// RenderOptions controls output behavior for the rendering boundary.
|
|
13
|
+
type RenderOptions struct {
|
|
14
|
+
Format string
|
|
15
|
+
Verbose bool
|
|
16
|
+
AllFields bool
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Renderer defines the rendering boundary all commands should use.
|
|
20
|
+
type Renderer interface {
|
|
21
|
+
Render(w io.Writer, result NormalizedResult, opts RenderOptions) error
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type defaultRenderer struct{}
|
|
25
|
+
|
|
26
|
+
// NewRenderer returns the default rendering implementation.
|
|
27
|
+
func NewRenderer() Renderer {
|
|
28
|
+
return &defaultRenderer{}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Render writes a normalized result using the requested output format.
|
|
32
|
+
func Render(w io.Writer, result NormalizedResult, opts RenderOptions) error {
|
|
33
|
+
return NewRenderer().Render(w, result, opts)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
func (r *defaultRenderer) Render(w io.Writer, result NormalizedResult, opts RenderOptions) error {
|
|
37
|
+
format := strings.ToLower(strings.TrimSpace(opts.Format))
|
|
38
|
+
if format == "" {
|
|
39
|
+
format = "text"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
switch format {
|
|
43
|
+
case "text":
|
|
44
|
+
return renderText(w, result, opts)
|
|
45
|
+
case "json":
|
|
46
|
+
return renderJSON(w, result, opts)
|
|
47
|
+
case "jsonl":
|
|
48
|
+
return renderJSONL(w, result, opts)
|
|
49
|
+
default:
|
|
50
|
+
return fmt.Errorf("unsupported render format %q", opts.Format)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
func renderJSON(w io.Writer, result NormalizedResult, opts RenderOptions) error {
|
|
55
|
+
sanitized, err := sanitizeValue(result, opts.AllFields)
|
|
56
|
+
if err != nil {
|
|
57
|
+
return err
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
encoded, err := json.MarshalIndent(sanitized, "", " ")
|
|
61
|
+
if err != nil {
|
|
62
|
+
return fmt.Errorf("encode json output: %w", err)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if _, err := fmt.Fprintln(w, string(encoded)); err != nil {
|
|
66
|
+
return fmt.Errorf("write json output: %w", err)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return nil
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
func renderJSONL(w io.Writer, result NormalizedResult, opts RenderOptions) error {
|
|
73
|
+
if len(result.Items) == 0 {
|
|
74
|
+
switch result.Status {
|
|
75
|
+
case ResultStatusEmpty:
|
|
76
|
+
return nil
|
|
77
|
+
case ResultStatusError:
|
|
78
|
+
meta := map[string]any{
|
|
79
|
+
"_meta": map[string]any{
|
|
80
|
+
"kind": result.Kind,
|
|
81
|
+
"status": result.Status,
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
if result.Message != "" {
|
|
85
|
+
meta["_meta"].(map[string]any)["message"] = result.Message
|
|
86
|
+
}
|
|
87
|
+
if result.Error != nil {
|
|
88
|
+
meta["_meta"].(map[string]any)["error"] = result.Error
|
|
89
|
+
}
|
|
90
|
+
return writeJSONLLine(w, meta, opts.AllFields)
|
|
91
|
+
default:
|
|
92
|
+
if result.Data != nil {
|
|
93
|
+
return fmt.Errorf("jsonl format requires list results")
|
|
94
|
+
}
|
|
95
|
+
return nil
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if result.Data != nil {
|
|
100
|
+
return fmt.Errorf("jsonl format requires list results")
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if result.Status == ResultStatusPartial || len(result.Warnings) > 0 || result.Message != "" {
|
|
104
|
+
meta := map[string]any{
|
|
105
|
+
"_meta": map[string]any{
|
|
106
|
+
"kind": result.Kind,
|
|
107
|
+
"status": result.Status,
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
if len(result.Warnings) > 0 {
|
|
111
|
+
meta["_meta"].(map[string]any)["warnings"] = result.Warnings
|
|
112
|
+
}
|
|
113
|
+
if result.Message != "" {
|
|
114
|
+
meta["_meta"].(map[string]any)["message"] = result.Message
|
|
115
|
+
}
|
|
116
|
+
if err := writeJSONLLine(w, meta, opts.AllFields); err != nil {
|
|
117
|
+
return err
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for _, item := range result.Items {
|
|
122
|
+
if err := writeJSONLLine(w, item, opts.AllFields); err != nil {
|
|
123
|
+
return err
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return nil
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
func writeJSONLLine(w io.Writer, value any, allFields bool) error {
|
|
131
|
+
sanitized, err := sanitizeValue(value, allFields)
|
|
132
|
+
if err != nil {
|
|
133
|
+
return err
|
|
134
|
+
}
|
|
135
|
+
encoded, err := json.Marshal(sanitized)
|
|
136
|
+
if err != nil {
|
|
137
|
+
return fmt.Errorf("encode jsonl line: %w", err)
|
|
138
|
+
}
|
|
139
|
+
if _, err := fmt.Fprintln(w, string(encoded)); err != nil {
|
|
140
|
+
return fmt.Errorf("write jsonl line: %w", err)
|
|
141
|
+
}
|
|
142
|
+
return nil
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
func renderText(w io.Writer, result NormalizedResult, opts RenderOptions) error {
|
|
146
|
+
lines := make([]string, 0, 16)
|
|
147
|
+
kindTitle := titleize(string(result.Kind))
|
|
148
|
+
|
|
149
|
+
switch result.Status {
|
|
150
|
+
case ResultStatusError:
|
|
151
|
+
message := result.Message
|
|
152
|
+
if message == "" {
|
|
153
|
+
message = "transport error"
|
|
154
|
+
}
|
|
155
|
+
lines = append(lines, fmt.Sprintf("Error (%s): %s", kindTitle, message))
|
|
156
|
+
if result.Error != nil {
|
|
157
|
+
if result.Error.URL != "" {
|
|
158
|
+
lines = append(lines, "URL: "+result.Error.URL)
|
|
159
|
+
}
|
|
160
|
+
if result.Error.StatusCode > 0 {
|
|
161
|
+
lines = append(lines, fmt.Sprintf("Status: %d", result.Error.StatusCode))
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if result.RequestedRef != "" {
|
|
165
|
+
lines = append(lines, "Requested: "+result.RequestedRef)
|
|
166
|
+
}
|
|
167
|
+
return writeTextLines(w, lines)
|
|
168
|
+
case ResultStatusPartial:
|
|
169
|
+
warningLine := "Partial data returned"
|
|
170
|
+
if len(result.Warnings) > 0 {
|
|
171
|
+
warningLine = warningLine + ": " + strings.Join(sanitizeWarningsForText(result.Warnings), "; ")
|
|
172
|
+
}
|
|
173
|
+
lines = append(lines, warningLine)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if result.Data != nil {
|
|
177
|
+
if result.Kind == EntityMatchScorecard {
|
|
178
|
+
itemMap, err := toMap(result.Data, opts.AllFields)
|
|
179
|
+
if err != nil {
|
|
180
|
+
return err
|
|
181
|
+
}
|
|
182
|
+
lines = append(lines, "Match Scorecard")
|
|
183
|
+
lines = append(lines, formatMatchScorecard(itemMap)...)
|
|
184
|
+
return writeTextLines(w, lines)
|
|
185
|
+
}
|
|
186
|
+
if result.Kind == EntityTeamLeaders {
|
|
187
|
+
itemMap, err := toMap(result.Data, opts.AllFields)
|
|
188
|
+
if err != nil {
|
|
189
|
+
return err
|
|
190
|
+
}
|
|
191
|
+
lines = append(lines, "Team Leaders")
|
|
192
|
+
lines = append(lines, formatTeamLeaders(itemMap)...)
|
|
193
|
+
return writeTextLines(w, lines)
|
|
194
|
+
}
|
|
195
|
+
if result.Kind == EntityInnings {
|
|
196
|
+
itemMap, err := toMap(result.Data, opts.AllFields)
|
|
197
|
+
if err != nil {
|
|
198
|
+
return err
|
|
199
|
+
}
|
|
200
|
+
lines = append(lines, "Innings")
|
|
201
|
+
lines = append(lines, formatInningsTimelines(itemMap)...)
|
|
202
|
+
return writeTextLines(w, lines)
|
|
203
|
+
}
|
|
204
|
+
if result.Kind == EntityPlayerStats {
|
|
205
|
+
itemMap, err := toMap(result.Data, opts.AllFields)
|
|
206
|
+
if err != nil {
|
|
207
|
+
return err
|
|
208
|
+
}
|
|
209
|
+
lines = append(lines, "Player Statistics")
|
|
210
|
+
lines = append(lines, formatPlayerStatistics(itemMap)...)
|
|
211
|
+
return writeTextLines(w, lines)
|
|
212
|
+
}
|
|
213
|
+
if result.Kind == EntityAnalysisDismiss || result.Kind == EntityAnalysisBowl || result.Kind == EntityAnalysisBat || result.Kind == EntityAnalysisPart {
|
|
214
|
+
itemMap, err := toMap(result.Data, opts.AllFields)
|
|
215
|
+
if err != nil {
|
|
216
|
+
return err
|
|
217
|
+
}
|
|
218
|
+
lines = append(lines, "Analysis")
|
|
219
|
+
lines = append(lines, formatAnalysisView(itemMap)...)
|
|
220
|
+
return writeTextLines(w, lines)
|
|
221
|
+
}
|
|
222
|
+
if result.Kind == EntityPlayer {
|
|
223
|
+
itemMap, err := toMap(result.Data, opts.AllFields)
|
|
224
|
+
if err != nil {
|
|
225
|
+
return err
|
|
226
|
+
}
|
|
227
|
+
lines = append(lines, "Player")
|
|
228
|
+
lines = append(lines, formatPlayerProfile(itemMap)...)
|
|
229
|
+
return writeTextLines(w, lines)
|
|
230
|
+
}
|
|
231
|
+
if result.Kind == EntityPlayerMatch {
|
|
232
|
+
itemMap, err := toMap(result.Data, opts.AllFields)
|
|
233
|
+
if err != nil {
|
|
234
|
+
return err
|
|
235
|
+
}
|
|
236
|
+
lines = append(lines, "Player Match")
|
|
237
|
+
lines = append(lines, formatPlayerMatchView(itemMap)...)
|
|
238
|
+
return writeTextLines(w, lines)
|
|
239
|
+
}
|
|
240
|
+
if result.Kind == EntityCompMetadata {
|
|
241
|
+
itemMap, err := toMap(result.Data, opts.AllFields)
|
|
242
|
+
if err != nil {
|
|
243
|
+
return err
|
|
244
|
+
}
|
|
245
|
+
lines = append(lines, "Competition Metadata")
|
|
246
|
+
lines = append(lines, formatCompetitionMetadata(itemMap)...)
|
|
247
|
+
return writeTextLines(w, lines)
|
|
248
|
+
}
|
|
249
|
+
if result.Kind == EntityMatch || result.Kind == EntityCompetition {
|
|
250
|
+
itemMap, err := toMap(result.Data, opts.AllFields)
|
|
251
|
+
if err != nil {
|
|
252
|
+
return err
|
|
253
|
+
}
|
|
254
|
+
lines = append(lines, kindTitle)
|
|
255
|
+
lines = append(lines, formatMatchView(itemMap)...)
|
|
256
|
+
return writeTextLines(w, lines)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
itemMap, err := toMap(result.Data, opts.AllFields)
|
|
260
|
+
if err != nil {
|
|
261
|
+
return err
|
|
262
|
+
}
|
|
263
|
+
lines = append(lines, fmt.Sprintf("%s", kindTitle))
|
|
264
|
+
lines = append(lines, formatSingleEntity(itemMap, result.Kind, opts)...)
|
|
265
|
+
return writeTextLines(w, lines)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if len(result.Items) == 0 {
|
|
269
|
+
message := result.Message
|
|
270
|
+
if message == "" {
|
|
271
|
+
message = fmt.Sprintf("No %s found.", kindPlural(result.Kind))
|
|
272
|
+
}
|
|
273
|
+
lines = append(lines, sentenceCase(message))
|
|
274
|
+
return writeTextLines(w, lines)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
lines = append(lines, fmt.Sprintf("%s (%d)", titleize(kindPlural(result.Kind)), len(result.Items)))
|
|
278
|
+
for i, item := range result.Items {
|
|
279
|
+
itemMap, err := toMap(item, opts.AllFields)
|
|
280
|
+
if err != nil {
|
|
281
|
+
return err
|
|
282
|
+
}
|
|
283
|
+
lines = append(lines, fmt.Sprintf("%d. %s", i+1, summarizeEntity(itemMap, result.Kind, opts.Verbose)))
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return writeTextLines(w, lines)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
func writeTextLines(w io.Writer, lines []string) error {
|
|
290
|
+
for _, line := range lines {
|
|
291
|
+
if _, err := fmt.Fprintln(w, line); err != nil {
|
|
292
|
+
return fmt.Errorf("write text output: %w", err)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return nil
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
func sanitizeValue(value any, allFields bool) (any, error) {
|
|
299
|
+
blob, err := json.Marshal(value)
|
|
300
|
+
if err != nil {
|
|
301
|
+
return nil, fmt.Errorf("marshal value: %w", err)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
var out any
|
|
305
|
+
if err := json.Unmarshal(blob, &out); err != nil {
|
|
306
|
+
return nil, fmt.Errorf("unmarshal value: %w", err)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if !allFields {
|
|
310
|
+
removeExtensions(out)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return out, nil
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
func removeExtensions(value any) {
|
|
317
|
+
switch typed := value.(type) {
|
|
318
|
+
case map[string]any:
|
|
319
|
+
delete(typed, "extensions")
|
|
320
|
+
for _, child := range typed {
|
|
321
|
+
removeExtensions(child)
|
|
322
|
+
}
|
|
323
|
+
case []any:
|
|
324
|
+
for _, child := range typed {
|
|
325
|
+
removeExtensions(child)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
func toMap(value any, allFields bool) (map[string]any, error) {
|
|
331
|
+
sanitized, err := sanitizeValue(value, allFields)
|
|
332
|
+
if err != nil {
|
|
333
|
+
return nil, err
|
|
334
|
+
}
|
|
335
|
+
mapped, ok := sanitized.(map[string]any)
|
|
336
|
+
if !ok {
|
|
337
|
+
return nil, fmt.Errorf("render item is not an object")
|
|
338
|
+
}
|
|
339
|
+
return mapped, nil
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
func summarizeEntity(entity map[string]any, kind EntityKind, verbose bool) string {
|
|
343
|
+
switch kind {
|
|
344
|
+
case EntityMatch:
|
|
345
|
+
id := valueString(entity, "id")
|
|
346
|
+
desc := firstNonEmpty(valueString(entity, "shortDescription"), valueString(entity, "description"))
|
|
347
|
+
if desc == "" {
|
|
348
|
+
desc = valueString(entity, "note")
|
|
349
|
+
}
|
|
350
|
+
if desc == "" {
|
|
351
|
+
desc = valueString(entity, "date")
|
|
352
|
+
}
|
|
353
|
+
teams := matchTeamsLabel(entity)
|
|
354
|
+
state := valueString(entity, "matchState")
|
|
355
|
+
score := valueString(entity, "scoreSummary")
|
|
356
|
+
venue := firstNonEmpty(valueString(entity, "venueName"), valueString(entity, "venueSummary"))
|
|
357
|
+
date := valueString(entity, "date")
|
|
358
|
+
if verbose {
|
|
359
|
+
return joinParts(
|
|
360
|
+
id,
|
|
361
|
+
desc,
|
|
362
|
+
teams,
|
|
363
|
+
state,
|
|
364
|
+
score,
|
|
365
|
+
date,
|
|
366
|
+
venue,
|
|
367
|
+
"league "+valueString(entity, "leagueId"),
|
|
368
|
+
"event "+valueString(entity, "eventId"),
|
|
369
|
+
)
|
|
370
|
+
}
|
|
371
|
+
return joinParts(id, desc, teams, state, score, date)
|
|
372
|
+
case EntityMatchScorecard:
|
|
373
|
+
return joinParts(
|
|
374
|
+
fmt.Sprintf("batting %d", len(sliceValue(entity, "battingCards"))),
|
|
375
|
+
fmt.Sprintf("bowling %d", len(sliceValue(entity, "bowlingCards"))),
|
|
376
|
+
fmt.Sprintf("partnerships %d", len(sliceValue(entity, "partnershipCards"))),
|
|
377
|
+
)
|
|
378
|
+
case EntityMatchSituation:
|
|
379
|
+
if data := valueString(entity, "data"); data != "" {
|
|
380
|
+
return joinParts("situation", data)
|
|
381
|
+
}
|
|
382
|
+
return joinParts("situation", valueString(entity, "competitionId"))
|
|
383
|
+
case EntityCompetition:
|
|
384
|
+
return joinParts(
|
|
385
|
+
firstNonEmpty(valueString(entity, "shortDescription"), valueString(entity, "description"), valueString(entity, "id")),
|
|
386
|
+
matchTeamsLabel(entity),
|
|
387
|
+
valueString(entity, "matchState"),
|
|
388
|
+
)
|
|
389
|
+
case EntityCompOfficial, EntityCompBroadcast, EntityCompTicket, EntityCompOdds:
|
|
390
|
+
return joinParts(
|
|
391
|
+
firstNonEmpty(valueString(entity, "displayName"), valueString(entity, "name"), valueString(entity, "text"), valueString(entity, "value"), valueString(entity, "id")),
|
|
392
|
+
valueString(entity, "role"),
|
|
393
|
+
valueString(entity, "type"),
|
|
394
|
+
)
|
|
395
|
+
case EntityCompMetadata:
|
|
396
|
+
return joinParts(
|
|
397
|
+
"officials "+fmt.Sprintf("%d", len(sliceValue(entity, "officials"))),
|
|
398
|
+
"broadcasts "+fmt.Sprintf("%d", len(sliceValue(entity, "broadcasts"))),
|
|
399
|
+
"tickets "+fmt.Sprintf("%d", len(sliceValue(entity, "tickets"))),
|
|
400
|
+
"odds "+fmt.Sprintf("%d", len(sliceValue(entity, "odds"))),
|
|
401
|
+
)
|
|
402
|
+
case EntityPlayer:
|
|
403
|
+
return joinParts(firstNonEmpty(valueString(entity, "displayName"), valueString(entity, "fullName"), valueString(entity, "name")), bracket(valueString(entity, "id")))
|
|
404
|
+
case EntityPlayerStats:
|
|
405
|
+
return joinParts(firstNonEmpty(valueString(entity, "name"), "statistics"), fmt.Sprintf("%d categories", len(sliceValue(entity, "categories"))))
|
|
406
|
+
case EntityPlayerMatch:
|
|
407
|
+
return joinParts(
|
|
408
|
+
firstNonEmpty(valueString(entity, "playerName"), valueString(entity, "playerId")),
|
|
409
|
+
"match "+valueString(entity, "matchId"),
|
|
410
|
+
"bat "+fmt.Sprintf("%d", len(sliceValue(entity, "batting"))),
|
|
411
|
+
"bowl "+fmt.Sprintf("%d", len(sliceValue(entity, "bowling"))),
|
|
412
|
+
)
|
|
413
|
+
case EntityPlayerInnings:
|
|
414
|
+
return joinParts(
|
|
415
|
+
firstNonEmpty(valueString(entity, "playerName"), valueString(entity, "playerId")),
|
|
416
|
+
"innings "+valueString(entity, "inningsNumber")+"/"+valueString(entity, "period"),
|
|
417
|
+
firstNonEmpty(valueString(entity, "teamName"), valueString(entity, "teamId")),
|
|
418
|
+
)
|
|
419
|
+
case EntityPlayerDismissal:
|
|
420
|
+
return joinParts(
|
|
421
|
+
firstNonEmpty(valueString(entity, "dismissalName"), valueString(entity, "dismissalType")),
|
|
422
|
+
firstNonEmpty(valueString(entity, "dismissalCard"), valueString(entity, "fow")),
|
|
423
|
+
valueString(entity, "detailRef"),
|
|
424
|
+
)
|
|
425
|
+
case EntityPlayerDelivery:
|
|
426
|
+
short := firstNonEmpty(valueString(entity, "shortText"), valueString(entity, "text"))
|
|
427
|
+
return joinParts(short, involvementLabel(entity), overBallLabel(entity))
|
|
428
|
+
case EntityNewsArticle:
|
|
429
|
+
return joinParts(firstNonEmpty(valueString(entity, "headline"), valueString(entity, "title"), valueString(entity, "id")), valueString(entity, "published"), valueString(entity, "byline"))
|
|
430
|
+
case EntityTeam:
|
|
431
|
+
name := firstNonEmpty(valueString(entity, "name"), valueString(entity, "shortName"), valueString(entity, "id"))
|
|
432
|
+
return joinParts(name, bracket(valueString(entity, "homeAway")))
|
|
433
|
+
case EntityTeamRoster:
|
|
434
|
+
name := firstNonEmpty(valueString(entity, "displayName"), valueString(entity, "playerId"), valueString(entity, "athleteId"))
|
|
435
|
+
badges := []string{}
|
|
436
|
+
if valueString(entity, "captain") == "true" {
|
|
437
|
+
badges = append(badges, "captain")
|
|
438
|
+
}
|
|
439
|
+
if valueString(entity, "active") == "true" {
|
|
440
|
+
badges = append(badges, "active")
|
|
441
|
+
}
|
|
442
|
+
return joinParts(name, strings.Join(badges, ", "))
|
|
443
|
+
case EntityTeamScore:
|
|
444
|
+
return joinParts(valueString(entity, "displayValue"), valueString(entity, "value"), bracket(valueString(entity, "source")))
|
|
445
|
+
case EntityTeamLeaders:
|
|
446
|
+
return joinParts(valueString(entity, "name"), fmt.Sprintf("%d categories", len(sliceValue(entity, "categories"))))
|
|
447
|
+
case EntityTeamStatistics, EntityTeamRecords:
|
|
448
|
+
return joinParts(firstNonEmpty(valueString(entity, "displayName"), valueString(entity, "name")), fmt.Sprintf("%d stats", len(sliceValue(entity, "stats"))))
|
|
449
|
+
case EntityLeague:
|
|
450
|
+
return joinParts(firstNonEmpty(valueString(entity, "name"), valueString(entity, "id")), bracket(valueString(entity, "slug")))
|
|
451
|
+
case EntitySeason:
|
|
452
|
+
return joinParts(valueString(entity, "id"), valueString(entity, "leagueId"))
|
|
453
|
+
case EntityCalendarDay:
|
|
454
|
+
return joinParts(
|
|
455
|
+
valueString(entity, "date"),
|
|
456
|
+
valueString(entity, "dayType"),
|
|
457
|
+
strings.Join(stringSliceValue(entity, "sections"), ", "),
|
|
458
|
+
)
|
|
459
|
+
case EntitySeasonType:
|
|
460
|
+
return joinParts(
|
|
461
|
+
firstNonEmpty(valueString(entity, "name"), "type "+valueString(entity, "id")),
|
|
462
|
+
"season "+valueString(entity, "seasonId"),
|
|
463
|
+
)
|
|
464
|
+
case EntitySeasonGroup:
|
|
465
|
+
return joinParts(
|
|
466
|
+
firstNonEmpty(valueString(entity, "name"), "group "+valueString(entity, "id")),
|
|
467
|
+
"type "+valueString(entity, "typeId"),
|
|
468
|
+
"season "+valueString(entity, "seasonId"),
|
|
469
|
+
)
|
|
470
|
+
case EntityStandingsGroup:
|
|
471
|
+
return joinParts(valueString(entity, "id"), "season "+valueString(entity, "seasonId"))
|
|
472
|
+
case EntityInnings:
|
|
473
|
+
score := valueString(entity, "score")
|
|
474
|
+
if score == "" {
|
|
475
|
+
score = joinParts(valueString(entity, "runs")+"/"+valueString(entity, "wickets"), valueString(entity, "overs")+" ov")
|
|
476
|
+
}
|
|
477
|
+
return joinParts(
|
|
478
|
+
firstNonEmpty(valueString(entity, "teamName"), valueString(entity, "teamId")),
|
|
479
|
+
"innings "+valueString(entity, "inningsNumber")+"/"+valueString(entity, "period"),
|
|
480
|
+
score,
|
|
481
|
+
fmt.Sprintf("%d wickets", len(sliceValue(entity, "wicketTimeline"))),
|
|
482
|
+
)
|
|
483
|
+
case EntityDeliveryEvent:
|
|
484
|
+
short := firstNonEmpty(valueString(entity, "shortText"), valueString(entity, "text"))
|
|
485
|
+
if short == "" {
|
|
486
|
+
short = joinParts("over "+valueString(entity, "overNumber"), "ball "+valueString(entity, "ballNumber"))
|
|
487
|
+
}
|
|
488
|
+
return short
|
|
489
|
+
case EntityStatCategory:
|
|
490
|
+
return joinParts(firstNonEmpty(valueString(entity, "displayName"), valueString(entity, "name")), fmt.Sprintf("%d stats", len(sliceValue(entity, "stats"))))
|
|
491
|
+
case EntityPartnership:
|
|
492
|
+
return joinParts(
|
|
493
|
+
firstNonEmpty(valueString(entity, "wicketName"), "partnership "+valueString(entity, "id")),
|
|
494
|
+
valueString(entity, "runs")+" runs",
|
|
495
|
+
valueString(entity, "overs")+" ov",
|
|
496
|
+
"innings "+valueString(entity, "inningsId")+"/"+valueString(entity, "period"),
|
|
497
|
+
)
|
|
498
|
+
case EntityFallOfWicket:
|
|
499
|
+
return joinParts(
|
|
500
|
+
"wicket "+valueString(entity, "wicketNumber"),
|
|
501
|
+
valueString(entity, "runs")+"/"+valueString(entity, "wicketNumber"),
|
|
502
|
+
valueString(entity, "wicketOver")+" ov",
|
|
503
|
+
"innings "+valueString(entity, "inningsId")+"/"+valueString(entity, "period"),
|
|
504
|
+
)
|
|
505
|
+
case EntityAnalysisDismiss, EntityAnalysisBowl, EntityAnalysisBat, EntityAnalysisPart:
|
|
506
|
+
return joinParts(
|
|
507
|
+
valueString(entity, "key"),
|
|
508
|
+
valueString(entity, "metric"),
|
|
509
|
+
valueString(entity, "value"),
|
|
510
|
+
)
|
|
511
|
+
default:
|
|
512
|
+
if summary := valueString(entity, "id"); summary != "" {
|
|
513
|
+
return summary
|
|
514
|
+
}
|
|
515
|
+
return "item"
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
func formatSingleEntity(entity map[string]any, kind EntityKind, opts RenderOptions) []string {
|
|
520
|
+
order := []string{}
|
|
521
|
+
switch kind {
|
|
522
|
+
case EntityMatch:
|
|
523
|
+
order = []string{
|
|
524
|
+
"id", "competitionId", "eventId", "leagueId",
|
|
525
|
+
"description", "shortDescription", "matchState",
|
|
526
|
+
"date", "endDate", "venueName", "venueSummary", "scoreSummary",
|
|
527
|
+
"teams",
|
|
528
|
+
}
|
|
529
|
+
case EntityCompetition:
|
|
530
|
+
order = []string{
|
|
531
|
+
"id", "competitionId", "eventId", "leagueId",
|
|
532
|
+
"description", "shortDescription", "matchState",
|
|
533
|
+
"date", "endDate", "venueName", "venueSummary", "scoreSummary",
|
|
534
|
+
"teams",
|
|
535
|
+
}
|
|
536
|
+
case EntityCompOfficial, EntityCompBroadcast, EntityCompTicket, EntityCompOdds:
|
|
537
|
+
order = []string{
|
|
538
|
+
"id", "displayName", "name", "role", "type", "order", "text", "value", "href",
|
|
539
|
+
}
|
|
540
|
+
case EntityCompMetadata:
|
|
541
|
+
order = []string{
|
|
542
|
+
"competition", "officials", "broadcasts", "tickets", "odds",
|
|
543
|
+
}
|
|
544
|
+
case EntityPlayer:
|
|
545
|
+
order = []string{
|
|
546
|
+
"id", "displayName", "fullName", "name", "firstName", "middleName", "lastName",
|
|
547
|
+
"battingName", "fieldingName", "gender", "age", "dateOfBirthDisplay",
|
|
548
|
+
"position", "team", "majorTeams", "debuts", "newsRef",
|
|
549
|
+
}
|
|
550
|
+
case EntityPlayerStats:
|
|
551
|
+
order = []string{"playerId", "name", "abbreviation", "splitId", "categories"}
|
|
552
|
+
case EntityPlayerMatch:
|
|
553
|
+
order = []string{
|
|
554
|
+
"playerId", "playerName", "matchId", "teamId", "teamName",
|
|
555
|
+
"summary", "batting", "bowling", "fielding",
|
|
556
|
+
}
|
|
557
|
+
case EntityPlayerInnings:
|
|
558
|
+
order = []string{
|
|
559
|
+
"playerId", "playerName", "matchId", "teamId", "teamName",
|
|
560
|
+
"inningsNumber", "period", "order", "isBatting", "summary",
|
|
561
|
+
"batting", "bowling", "fielding",
|
|
562
|
+
}
|
|
563
|
+
case EntityPlayerDismissal:
|
|
564
|
+
order = []string{
|
|
565
|
+
"playerId", "playerName", "matchId", "teamId", "teamName",
|
|
566
|
+
"inningsNumber", "period", "wicketNumber", "fow", "over",
|
|
567
|
+
"dismissalName", "dismissalCard", "dismissalType", "dismissalText",
|
|
568
|
+
"ballsFaced", "strikeRate", "batsmanPlayerId", "bowlerPlayerId", "fielderPlayerId",
|
|
569
|
+
"detailRef", "detailShortText",
|
|
570
|
+
}
|
|
571
|
+
case EntityPlayerDelivery:
|
|
572
|
+
order = []string{
|
|
573
|
+
"id", "matchId", "teamId", "period", "overNumber", "ballNumber", "scoreValue",
|
|
574
|
+
"shortText", "dismissalType", "dismissalName", "dismissalCard",
|
|
575
|
+
"batsmanPlayerId", "bowlerPlayerId", "fielderPlayerId",
|
|
576
|
+
"xCoordinate", "yCoordinate", "involvement",
|
|
577
|
+
}
|
|
578
|
+
case EntityNewsArticle:
|
|
579
|
+
order = []string{"id", "headline", "title", "byline", "published", "description", "webUrl"}
|
|
580
|
+
case EntityTeam:
|
|
581
|
+
order = []string{"id", "name", "shortName", "homeAway"}
|
|
582
|
+
case EntityTeamScore:
|
|
583
|
+
order = []string{"teamId", "matchId", "scope", "displayValue", "value", "winner", "source"}
|
|
584
|
+
case EntityLeague:
|
|
585
|
+
order = []string{"id", "name", "slug"}
|
|
586
|
+
case EntitySeason:
|
|
587
|
+
order = []string{"id", "year", "leagueId"}
|
|
588
|
+
case EntityCalendarDay:
|
|
589
|
+
order = []string{"date", "dayType", "sections", "startDate", "endDate", "leagueId"}
|
|
590
|
+
case EntitySeasonType:
|
|
591
|
+
order = []string{"id", "name", "abbreviation", "seasonId", "leagueId", "startDate", "endDate", "hasGroups", "hasStandings", "groupsRef"}
|
|
592
|
+
case EntitySeasonGroup:
|
|
593
|
+
order = []string{"id", "name", "abbreviation", "typeId", "seasonId", "leagueId", "standingsRef"}
|
|
594
|
+
case EntityStandingsGroup:
|
|
595
|
+
order = []string{"id", "seasonId", "groupId"}
|
|
596
|
+
case EntityInnings:
|
|
597
|
+
order = []string{
|
|
598
|
+
"teamName", "teamId", "matchId", "inningsNumber", "period",
|
|
599
|
+
"runs", "wickets", "overs", "score", "description",
|
|
600
|
+
"statisticsRef", "partnershipsRef", "fallOfWicketRef",
|
|
601
|
+
"overTimeline", "wicketTimeline",
|
|
602
|
+
}
|
|
603
|
+
case EntityDeliveryEvent:
|
|
604
|
+
order = []string{
|
|
605
|
+
"id", "period", "overNumber", "ballNumber", "scoreValue", "shortText",
|
|
606
|
+
"playType", "dismissal", "dismissalType", "bbbTimestamp", "xCoordinate", "yCoordinate",
|
|
607
|
+
}
|
|
608
|
+
case EntityMatchScorecard:
|
|
609
|
+
order = []string{"matchId", "competitionId", "eventId", "leagueId", "battingCards", "bowlingCards", "partnershipCards"}
|
|
610
|
+
case EntityMatchSituation:
|
|
611
|
+
order = []string{"matchId", "competitionId", "eventId", "leagueId", "oddsRef", "data"}
|
|
612
|
+
case EntityStatCategory:
|
|
613
|
+
order = []string{"name", "displayName", "abbreviation"}
|
|
614
|
+
case EntityPartnership:
|
|
615
|
+
order = []string{"teamName", "teamId", "inningsId", "period", "wicketNumber", "wicketName", "runs", "overs", "runRate", "batsmen"}
|
|
616
|
+
case EntityFallOfWicket:
|
|
617
|
+
order = []string{"teamName", "teamId", "inningsId", "period", "wicketNumber", "wicketOver", "runs", "runsScored", "ballsFaced", "athleteRef"}
|
|
618
|
+
case EntityAnalysisDismiss, EntityAnalysisBowl, EntityAnalysisBat, EntityAnalysisPart:
|
|
619
|
+
order = []string{
|
|
620
|
+
"command", "metric", "scope", "groupBy", "filters", "rows",
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
lines := make([]string, 0, len(order)+2)
|
|
625
|
+
for _, key := range order {
|
|
626
|
+
value := entity[key]
|
|
627
|
+
if isEmptyValue(value) {
|
|
628
|
+
continue
|
|
629
|
+
}
|
|
630
|
+
lines = append(lines, fmt.Sprintf("%s: %s", key, printableValue(value)))
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if opts.AllFields {
|
|
634
|
+
if extMap, ok := entity["extensions"].(map[string]any); ok && len(extMap) > 0 {
|
|
635
|
+
keys := mapsKeys(extMap)
|
|
636
|
+
sort.Strings(keys)
|
|
637
|
+
lines = append(lines, "extension fields: "+strings.Join(keys, ", "))
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if opts.Verbose {
|
|
642
|
+
if ref := valueString(entity, "ref"); ref != "" {
|
|
643
|
+
lines = append(lines, "ref: "+ref)
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return lines
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
func formatMatchScorecard(entity map[string]any) []string {
|
|
651
|
+
lines := make([]string, 0, 64)
|
|
652
|
+
|
|
653
|
+
if matchID := firstNonEmpty(valueString(entity, "matchId"), valueString(entity, "competitionId")); matchID != "" {
|
|
654
|
+
lines = append(lines, "Match: "+matchID)
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
batting := sliceValue(entity, "battingCards")
|
|
658
|
+
if len(batting) > 0 {
|
|
659
|
+
lines = append(lines, "Batting")
|
|
660
|
+
lines = append(lines, formatBattingCards(batting)...)
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
bowling := sliceValue(entity, "bowlingCards")
|
|
664
|
+
if len(bowling) > 0 {
|
|
665
|
+
lines = append(lines, "Bowling")
|
|
666
|
+
lines = append(lines, formatBowlingCards(bowling)...)
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
partnerships := sliceValue(entity, "partnershipCards")
|
|
670
|
+
if len(partnerships) > 0 {
|
|
671
|
+
lines = append(lines, "Partnerships")
|
|
672
|
+
lines = append(lines, formatPartnershipCards(partnerships)...)
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if len(batting) == 0 && len(bowling) == 0 && len(partnerships) == 0 {
|
|
676
|
+
lines = append(lines, "No scorecard sections available.")
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
return lines
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
func formatInningsTimelines(entity map[string]any) []string {
|
|
683
|
+
lines := make([]string, 0, 64)
|
|
684
|
+
|
|
685
|
+
if teamName := firstNonEmpty(valueString(entity, "teamName"), valueString(entity, "teamId")); teamName != "" {
|
|
686
|
+
lines = append(lines, "Team: "+teamName)
|
|
687
|
+
}
|
|
688
|
+
if matchID := firstNonEmpty(valueString(entity, "matchId"), valueString(entity, "competitionId")); matchID != "" {
|
|
689
|
+
lines = append(lines, "Match: "+matchID)
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
header := joinParts(
|
|
693
|
+
"Innings "+valueString(entity, "inningsNumber")+"/"+valueString(entity, "period"),
|
|
694
|
+
valueString(entity, "score"),
|
|
695
|
+
)
|
|
696
|
+
if strings.TrimSpace(header) == "" {
|
|
697
|
+
header = joinParts(
|
|
698
|
+
"Innings "+valueString(entity, "inningsNumber")+"/"+valueString(entity, "period"),
|
|
699
|
+
valueString(entity, "runs")+"/"+valueString(entity, "wickets"),
|
|
700
|
+
valueString(entity, "overs")+" ov",
|
|
701
|
+
)
|
|
702
|
+
}
|
|
703
|
+
if header != "" {
|
|
704
|
+
lines = append(lines, header)
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
overs := sliceValue(entity, "overTimeline")
|
|
708
|
+
if len(overs) > 0 {
|
|
709
|
+
lines = append(lines, "Over Timeline")
|
|
710
|
+
for _, rawOver := range overs {
|
|
711
|
+
over, ok := rawOver.(map[string]any)
|
|
712
|
+
if !ok {
|
|
713
|
+
continue
|
|
714
|
+
}
|
|
715
|
+
row := joinParts(
|
|
716
|
+
"Over "+valueString(over, "number"),
|
|
717
|
+
valueString(over, "runs")+" runs",
|
|
718
|
+
valueString(over, "wicketCount")+" wkts",
|
|
719
|
+
)
|
|
720
|
+
if row != "" {
|
|
721
|
+
lines = append(lines, " "+row)
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
wickets := sliceValue(entity, "wicketTimeline")
|
|
727
|
+
if len(wickets) > 0 {
|
|
728
|
+
lines = append(lines, "Wicket Timeline")
|
|
729
|
+
for idx, rawWicket := range wickets {
|
|
730
|
+
wicket, ok := rawWicket.(map[string]any)
|
|
731
|
+
if !ok {
|
|
732
|
+
continue
|
|
733
|
+
}
|
|
734
|
+
row := joinParts(
|
|
735
|
+
"#"+valueString(wicket, "number"),
|
|
736
|
+
valueString(wicket, "fow"),
|
|
737
|
+
valueString(wicket, "over")+" ov",
|
|
738
|
+
firstNonEmpty(valueString(wicket, "shortText"), valueString(wicket, "detailShortText")),
|
|
739
|
+
)
|
|
740
|
+
if row == "" {
|
|
741
|
+
continue
|
|
742
|
+
}
|
|
743
|
+
lines = append(lines, fmt.Sprintf(" %d. %s", idx+1, row))
|
|
744
|
+
if detailRef := valueString(wicket, "detailRef"); detailRef != "" {
|
|
745
|
+
lines = append(lines, " detail: "+detailRef)
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if len(overs) == 0 && len(wickets) == 0 {
|
|
751
|
+
lines = append(lines, "No period timeline data available.")
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
return lines
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
func formatPlayerProfile(entity map[string]any) []string {
|
|
758
|
+
lines := make([]string, 0, 16)
|
|
759
|
+
if name := firstNonEmpty(valueString(entity, "displayName"), valueString(entity, "fullName"), valueString(entity, "name")); name != "" {
|
|
760
|
+
lines = append(lines, "Name: "+name)
|
|
761
|
+
}
|
|
762
|
+
if playerID := valueString(entity, "id"); playerID != "" {
|
|
763
|
+
lines = append(lines, "Player ID: "+playerID)
|
|
764
|
+
}
|
|
765
|
+
if role := valueString(entity, "position"); role != "" {
|
|
766
|
+
lines = append(lines, "Role: "+role)
|
|
767
|
+
}
|
|
768
|
+
if team := namedValue(entity["team"]); team != "" && !looksLikeRawIdentifier(team) {
|
|
769
|
+
lines = append(lines, "Team: "+team)
|
|
770
|
+
}
|
|
771
|
+
if styles := styleSummary(entity); styles != "" {
|
|
772
|
+
lines = append(lines, "Styles: "+styles)
|
|
773
|
+
}
|
|
774
|
+
if majorTeams := sliceSummary(entity, "majorTeams", 4); majorTeams != "" {
|
|
775
|
+
lines = append(lines, "Major Teams: "+majorTeams)
|
|
776
|
+
}
|
|
777
|
+
if debuts := sliceSummary(entity, "debuts", 4); debuts != "" {
|
|
778
|
+
if strings.HasSuffix(debuts, " items") {
|
|
779
|
+
lines = append(lines, fmt.Sprintf("Debuts: %d matches", len(sliceValue(entity, "debuts"))))
|
|
780
|
+
} else {
|
|
781
|
+
lines = append(lines, "Debuts: "+debuts)
|
|
782
|
+
}
|
|
783
|
+
} else if debuts := sliceValue(entity, "debuts"); len(debuts) > 0 {
|
|
784
|
+
lines = append(lines, fmt.Sprintf("Debuts: %d matches", len(debuts)))
|
|
785
|
+
}
|
|
786
|
+
if born := valueString(entity, "dateOfBirthDisplay"); born != "" {
|
|
787
|
+
lines = append(lines, "Born: "+born)
|
|
788
|
+
}
|
|
789
|
+
return lines
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
func formatPlayerMatchView(entity map[string]any) []string {
|
|
793
|
+
lines := make([]string, 0, 16)
|
|
794
|
+
if player := valueString(entity, "playerName"); player != "" {
|
|
795
|
+
lines = append(lines, "Player: "+player)
|
|
796
|
+
}
|
|
797
|
+
if matchID := valueString(entity, "matchId"); matchID != "" {
|
|
798
|
+
lines = append(lines, "Match: "+matchID)
|
|
799
|
+
}
|
|
800
|
+
if team := valueString(entity, "teamName"); team != "" {
|
|
801
|
+
lines = append(lines, "Team: "+team)
|
|
802
|
+
}
|
|
803
|
+
if summary, ok := entity["summary"].(map[string]any); ok {
|
|
804
|
+
if text := summarizePlayerMatchSummary(summary); text != "" {
|
|
805
|
+
lines = append(lines, text)
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
if batting := sliceValue(entity, "batting"); len(batting) > 0 {
|
|
809
|
+
lines = append(lines, fmt.Sprintf("Batting Categories: %d", len(batting)))
|
|
810
|
+
}
|
|
811
|
+
if bowling := sliceValue(entity, "bowling"); len(bowling) > 0 {
|
|
812
|
+
lines = append(lines, fmt.Sprintf("Bowling Categories: %d", len(bowling)))
|
|
813
|
+
}
|
|
814
|
+
if fielding := sliceValue(entity, "fielding"); len(fielding) > 0 {
|
|
815
|
+
lines = append(lines, fmt.Sprintf("Fielding Categories: %d", len(fielding)))
|
|
816
|
+
}
|
|
817
|
+
return lines
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
func formatCompetitionMetadata(entity map[string]any) []string {
|
|
821
|
+
lines := make([]string, 0, 12)
|
|
822
|
+
if competition, ok := entity["competition"].(map[string]any); ok {
|
|
823
|
+
lines = append(lines, "Competition: "+joinParts(
|
|
824
|
+
firstNonEmpty(valueString(competition, "shortDescription"), valueString(competition, "description"), valueString(competition, "id")),
|
|
825
|
+
matchTeamsLabel(competition),
|
|
826
|
+
valueString(competition, "matchState"),
|
|
827
|
+
))
|
|
828
|
+
}
|
|
829
|
+
if officials := summarizeNamedItems(sliceValue(entity, "officials"), 5); officials != "" {
|
|
830
|
+
lines = append(lines, "Officials: "+officials)
|
|
831
|
+
}
|
|
832
|
+
if broadcasts := summarizeNamedItems(sliceValue(entity, "broadcasts"), 4); broadcasts != "" {
|
|
833
|
+
lines = append(lines, "Broadcasts: "+broadcasts)
|
|
834
|
+
}
|
|
835
|
+
if odds := summarizeNamedItems(sliceValue(entity, "odds"), 3); odds != "" {
|
|
836
|
+
lines = append(lines, "Odds: "+odds)
|
|
837
|
+
}
|
|
838
|
+
if tickets := sliceValue(entity, "tickets"); len(tickets) > 0 {
|
|
839
|
+
lines = append(lines, fmt.Sprintf("Tickets: %d options", len(tickets)))
|
|
840
|
+
}
|
|
841
|
+
return lines
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
func formatMatchView(entity map[string]any) []string {
|
|
845
|
+
lines := make([]string, 0, 16)
|
|
846
|
+
if id := valueString(entity, "id"); id != "" {
|
|
847
|
+
lines = append(lines, "Match: "+id)
|
|
848
|
+
}
|
|
849
|
+
if desc := firstNonEmpty(valueString(entity, "shortDescription"), valueString(entity, "description")); desc != "" {
|
|
850
|
+
lines = append(lines, "Fixture: "+desc)
|
|
851
|
+
}
|
|
852
|
+
if state := valueString(entity, "matchState"); state != "" {
|
|
853
|
+
lines = append(lines, "Status: "+state)
|
|
854
|
+
}
|
|
855
|
+
if score := valueString(entity, "scoreSummary"); score != "" {
|
|
856
|
+
lines = append(lines, "Score: "+score)
|
|
857
|
+
}
|
|
858
|
+
if venue := firstNonEmpty(valueString(entity, "venueName"), valueString(entity, "venueSummary")); venue != "" {
|
|
859
|
+
lines = append(lines, "Venue: "+venue)
|
|
860
|
+
}
|
|
861
|
+
if date := valueString(entity, "date"); date != "" {
|
|
862
|
+
lines = append(lines, "Date: "+date)
|
|
863
|
+
}
|
|
864
|
+
if teams := summarizeNamedItems(sliceValue(entity, "teams"), 4); teams != "" {
|
|
865
|
+
lines = append(lines, "Teams: "+teams)
|
|
866
|
+
}
|
|
867
|
+
return lines
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
func formatPlayerStatistics(entity map[string]any) []string {
|
|
871
|
+
lines := make([]string, 0, 64)
|
|
872
|
+
|
|
873
|
+
if playerID := valueString(entity, "playerId"); playerID != "" {
|
|
874
|
+
lines = append(lines, "Player: "+playerID)
|
|
875
|
+
}
|
|
876
|
+
if header := joinParts(firstNonEmpty(valueString(entity, "name"), "Statistics"), bracket(valueString(entity, "abbreviation"))); header != "" {
|
|
877
|
+
lines = append(lines, header)
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
categories := sliceValue(entity, "categories")
|
|
881
|
+
if len(categories) == 0 {
|
|
882
|
+
lines = append(lines, "No statistics categories available.")
|
|
883
|
+
return lines
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
for _, rawCategory := range categories {
|
|
887
|
+
category, ok := rawCategory.(map[string]any)
|
|
888
|
+
if !ok {
|
|
889
|
+
continue
|
|
890
|
+
}
|
|
891
|
+
categoryName := firstNonEmpty(valueString(category, "displayName"), valueString(category, "name"))
|
|
892
|
+
if categoryName == "" {
|
|
893
|
+
categoryName = "Category"
|
|
894
|
+
}
|
|
895
|
+
lines = append(lines, categoryName)
|
|
896
|
+
|
|
897
|
+
for idx, rawStat := range sliceValue(category, "stats") {
|
|
898
|
+
stat, ok := rawStat.(map[string]any)
|
|
899
|
+
if !ok {
|
|
900
|
+
continue
|
|
901
|
+
}
|
|
902
|
+
row := joinParts(
|
|
903
|
+
firstNonEmpty(valueString(stat, "displayName"), valueString(stat, "name")),
|
|
904
|
+
firstNonEmpty(valueString(stat, "displayValue"), valueString(stat, "value")),
|
|
905
|
+
bracket(valueString(stat, "abbreviation")),
|
|
906
|
+
)
|
|
907
|
+
if row == "" {
|
|
908
|
+
continue
|
|
909
|
+
}
|
|
910
|
+
lines = append(lines, fmt.Sprintf(" %d. %s", idx+1, row))
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
return lines
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
func formatBattingCards(cards []any) []string {
|
|
918
|
+
lines := make([]string, 0, len(cards)*6)
|
|
919
|
+
for _, rawCard := range cards {
|
|
920
|
+
card, ok := rawCard.(map[string]any)
|
|
921
|
+
if !ok {
|
|
922
|
+
continue
|
|
923
|
+
}
|
|
924
|
+
header := joinParts(
|
|
925
|
+
"Innings "+valueString(card, "inningsNumber"),
|
|
926
|
+
valueString(card, "teamName"),
|
|
927
|
+
valueString(card, "runs"),
|
|
928
|
+
valueString(card, "total"),
|
|
929
|
+
)
|
|
930
|
+
if strings.TrimSpace(header) != "" {
|
|
931
|
+
lines = append(lines, header)
|
|
932
|
+
}
|
|
933
|
+
for idx, rawPlayer := range sliceValue(card, "players") {
|
|
934
|
+
player, ok := rawPlayer.(map[string]any)
|
|
935
|
+
if !ok {
|
|
936
|
+
continue
|
|
937
|
+
}
|
|
938
|
+
score := valueString(player, "runs")
|
|
939
|
+
if balls := valueString(player, "ballsFaced"); balls != "" {
|
|
940
|
+
score = strings.TrimSpace(joinParts(score, "("+balls+" balls)"))
|
|
941
|
+
}
|
|
942
|
+
boundary := joinParts("4s "+valueString(player, "fours"), "6s "+valueString(player, "sixes"))
|
|
943
|
+
row := joinParts(valueString(player, "playerName"), score, boundary, valueString(player, "dismissal"))
|
|
944
|
+
if row == "" {
|
|
945
|
+
continue
|
|
946
|
+
}
|
|
947
|
+
lines = append(lines, fmt.Sprintf(" %d. %s", idx+1, row))
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return lines
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
func formatBowlingCards(cards []any) []string {
|
|
954
|
+
lines := make([]string, 0, len(cards)*6)
|
|
955
|
+
for _, rawCard := range cards {
|
|
956
|
+
card, ok := rawCard.(map[string]any)
|
|
957
|
+
if !ok {
|
|
958
|
+
continue
|
|
959
|
+
}
|
|
960
|
+
header := joinParts("Innings "+valueString(card, "inningsNumber"), valueString(card, "teamName"))
|
|
961
|
+
if strings.TrimSpace(header) != "" {
|
|
962
|
+
lines = append(lines, header)
|
|
963
|
+
}
|
|
964
|
+
for idx, rawPlayer := range sliceValue(card, "players") {
|
|
965
|
+
player, ok := rawPlayer.(map[string]any)
|
|
966
|
+
if !ok {
|
|
967
|
+
continue
|
|
968
|
+
}
|
|
969
|
+
figures := joinParts(
|
|
970
|
+
"overs "+valueString(player, "overs"),
|
|
971
|
+
"maidens "+valueString(player, "maidens"),
|
|
972
|
+
"runs "+valueString(player, "conceded"),
|
|
973
|
+
"wkts "+valueString(player, "wickets"),
|
|
974
|
+
"econ "+valueString(player, "economyRate"),
|
|
975
|
+
)
|
|
976
|
+
row := joinParts(valueString(player, "playerName"), figures, valueString(player, "nbw"))
|
|
977
|
+
if row == "" {
|
|
978
|
+
continue
|
|
979
|
+
}
|
|
980
|
+
lines = append(lines, fmt.Sprintf(" %d. %s", idx+1, row))
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
return lines
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
func formatPartnershipCards(cards []any) []string {
|
|
987
|
+
lines := make([]string, 0, len(cards)*6)
|
|
988
|
+
for _, rawCard := range cards {
|
|
989
|
+
card, ok := rawCard.(map[string]any)
|
|
990
|
+
if !ok {
|
|
991
|
+
continue
|
|
992
|
+
}
|
|
993
|
+
header := joinParts("Innings "+valueString(card, "inningsNumber"), valueString(card, "teamName"))
|
|
994
|
+
if strings.TrimSpace(header) != "" {
|
|
995
|
+
lines = append(lines, header)
|
|
996
|
+
}
|
|
997
|
+
for idx, rawPlayer := range sliceValue(card, "players") {
|
|
998
|
+
player, ok := rawPlayer.(map[string]any)
|
|
999
|
+
if !ok {
|
|
1000
|
+
continue
|
|
1001
|
+
}
|
|
1002
|
+
runs := valueString(player, "partnershipRuns")
|
|
1003
|
+
runsText := ""
|
|
1004
|
+
if runs != "" {
|
|
1005
|
+
runsText = runs + " runs"
|
|
1006
|
+
}
|
|
1007
|
+
overs := valueString(player, "partnershipOvers")
|
|
1008
|
+
oversText := ""
|
|
1009
|
+
if overs != "" {
|
|
1010
|
+
oversText = overs + " overs"
|
|
1011
|
+
}
|
|
1012
|
+
pair := joinParts(valueString(player, "player1Name"), valueString(player, "player2Name"))
|
|
1013
|
+
detail := joinParts(
|
|
1014
|
+
valueString(player, "partnershipWicketName"),
|
|
1015
|
+
runsText,
|
|
1016
|
+
oversText,
|
|
1017
|
+
)
|
|
1018
|
+
row := joinParts(pair, detail)
|
|
1019
|
+
if row == "" {
|
|
1020
|
+
continue
|
|
1021
|
+
}
|
|
1022
|
+
lines = append(lines, fmt.Sprintf(" %d. %s", idx+1, row))
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
return lines
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
func formatTeamLeaders(entity map[string]any) []string {
|
|
1029
|
+
lines := make([]string, 0, 64)
|
|
1030
|
+
|
|
1031
|
+
if teamName := firstNonEmpty(valueString(entity, "teamName"), valueString(entity, "name")); teamName != "" {
|
|
1032
|
+
lines = append(lines, "Team: "+teamName)
|
|
1033
|
+
}
|
|
1034
|
+
if matchID := valueString(entity, "matchId"); matchID != "" {
|
|
1035
|
+
lines = append(lines, "Match: "+matchID)
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
categories := sliceValue(entity, "categories")
|
|
1039
|
+
if len(categories) == 0 {
|
|
1040
|
+
lines = append(lines, "No leaderboard categories available.")
|
|
1041
|
+
return lines
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
batting := make([]string, 0)
|
|
1045
|
+
bowling := make([]string, 0)
|
|
1046
|
+
other := make([]string, 0)
|
|
1047
|
+
|
|
1048
|
+
for _, rawCategory := range categories {
|
|
1049
|
+
category, ok := rawCategory.(map[string]any)
|
|
1050
|
+
if !ok {
|
|
1051
|
+
continue
|
|
1052
|
+
}
|
|
1053
|
+
role := leaderCategoryRole(category)
|
|
1054
|
+
rows := formatTeamLeaderCategory(category, role)
|
|
1055
|
+
switch role {
|
|
1056
|
+
case "batting":
|
|
1057
|
+
batting = append(batting, rows...)
|
|
1058
|
+
case "bowling":
|
|
1059
|
+
bowling = append(bowling, rows...)
|
|
1060
|
+
default:
|
|
1061
|
+
other = append(other, rows...)
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
if len(batting) > 0 {
|
|
1066
|
+
lines = append(lines, "Batting Leaders")
|
|
1067
|
+
lines = append(lines, batting...)
|
|
1068
|
+
}
|
|
1069
|
+
if len(bowling) > 0 {
|
|
1070
|
+
lines = append(lines, "Bowling Leaders")
|
|
1071
|
+
lines = append(lines, bowling...)
|
|
1072
|
+
}
|
|
1073
|
+
if len(other) > 0 {
|
|
1074
|
+
lines = append(lines, "Other Leaders")
|
|
1075
|
+
lines = append(lines, other...)
|
|
1076
|
+
}
|
|
1077
|
+
if len(batting) == 0 && len(bowling) == 0 && len(other) == 0 {
|
|
1078
|
+
lines = append(lines, "No leaderboard categories available.")
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
return lines
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
func formatAnalysisView(entity map[string]any) []string {
|
|
1085
|
+
lines := make([]string, 0, 64)
|
|
1086
|
+
if command := valueString(entity, "command"); command != "" {
|
|
1087
|
+
lines = append(lines, "Command: "+command)
|
|
1088
|
+
}
|
|
1089
|
+
if metric := valueString(entity, "metric"); metric != "" {
|
|
1090
|
+
lines = append(lines, "Metric: "+metric)
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
scopeMap, _ := entity["scope"].(map[string]any)
|
|
1094
|
+
if scopeMap != nil {
|
|
1095
|
+
mode := valueString(scopeMap, "mode")
|
|
1096
|
+
league := firstNonEmpty(valueString(scopeMap, "requestedLeagueId"), valueString(scopeMap, "leagueName"), valueString(scopeMap, "leagueId"))
|
|
1097
|
+
matchCount := valueString(scopeMap, "matchCount")
|
|
1098
|
+
lines = append(lines, "Scope: "+joinParts(mode, league, "matches "+matchCount))
|
|
1099
|
+
if seasons := sliceValue(scopeMap, "seasons"); len(seasons) > 0 {
|
|
1100
|
+
seasonParts := make([]string, 0, len(seasons))
|
|
1101
|
+
for _, season := range seasons {
|
|
1102
|
+
if asString, ok := season.(string); ok && strings.TrimSpace(asString) != "" {
|
|
1103
|
+
seasonParts = append(seasonParts, strings.TrimSpace(asString))
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
if len(seasonParts) > 0 {
|
|
1107
|
+
lines = append(lines, "Seasons: "+strings.Join(seasonParts, ", "))
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
if groupBy := stringSliceValue(entity, "groupBy"); len(groupBy) > 0 {
|
|
1113
|
+
lines = append(lines, "Group By: "+strings.Join(groupBy, ", "))
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
rows := sliceValue(entity, "rows")
|
|
1117
|
+
if len(rows) == 0 {
|
|
1118
|
+
lines = append(lines, "No ranked rows found.")
|
|
1119
|
+
return lines
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
lines = append(lines, "Rows")
|
|
1123
|
+
for i, raw := range rows {
|
|
1124
|
+
row, ok := raw.(map[string]any)
|
|
1125
|
+
if !ok {
|
|
1126
|
+
continue
|
|
1127
|
+
}
|
|
1128
|
+
label := analysisRowLabel(row)
|
|
1129
|
+
line := joinParts(
|
|
1130
|
+
"#"+valueString(row, "rank"),
|
|
1131
|
+
label,
|
|
1132
|
+
valueString(row, "value"),
|
|
1133
|
+
)
|
|
1134
|
+
if count := valueString(row, "count"); count != "" {
|
|
1135
|
+
line = joinParts(line, "count "+count)
|
|
1136
|
+
}
|
|
1137
|
+
if matches := valueString(row, "matches"); matches != "" {
|
|
1138
|
+
line = joinParts(line, "matches "+matches)
|
|
1139
|
+
}
|
|
1140
|
+
if line == "" {
|
|
1141
|
+
continue
|
|
1142
|
+
}
|
|
1143
|
+
if valueString(row, "value") == "0" {
|
|
1144
|
+
continue
|
|
1145
|
+
}
|
|
1146
|
+
lines = append(lines, fmt.Sprintf(" %d. %s", i+1, line))
|
|
1147
|
+
}
|
|
1148
|
+
return lines
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
func formatTeamLeaderCategory(category map[string]any, role string) []string {
|
|
1152
|
+
lines := make([]string, 0, 24)
|
|
1153
|
+
name := firstNonEmpty(valueString(category, "displayName"), valueString(category, "name"))
|
|
1154
|
+
if name == "" {
|
|
1155
|
+
name = "Leaders"
|
|
1156
|
+
}
|
|
1157
|
+
lines = append(lines, name)
|
|
1158
|
+
|
|
1159
|
+
leaders := sliceValue(category, "leaders")
|
|
1160
|
+
for idx, rawLeader := range leaders {
|
|
1161
|
+
leader, ok := rawLeader.(map[string]any)
|
|
1162
|
+
if !ok {
|
|
1163
|
+
continue
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
player := firstNonEmpty(valueString(leader, "athleteName"), valueString(leader, "name"), "Unknown player")
|
|
1167
|
+
primary := valueString(leader, "displayValue")
|
|
1168
|
+
if primary == "" {
|
|
1169
|
+
primary = valueString(leader, "value")
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
score := ""
|
|
1173
|
+
switch role {
|
|
1174
|
+
case "batting":
|
|
1175
|
+
if primary != "" {
|
|
1176
|
+
score = primary + " runs"
|
|
1177
|
+
}
|
|
1178
|
+
case "bowling":
|
|
1179
|
+
if primary != "" {
|
|
1180
|
+
score = primary + " wkts"
|
|
1181
|
+
}
|
|
1182
|
+
default:
|
|
1183
|
+
score = primary
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
extras := []string{}
|
|
1187
|
+
if role == "batting" {
|
|
1188
|
+
if balls := valueString(leader, "balls"); balls != "" {
|
|
1189
|
+
extras = append(extras, balls+" balls")
|
|
1190
|
+
}
|
|
1191
|
+
if fours := valueString(leader, "fours"); fours != "" {
|
|
1192
|
+
extras = append(extras, fours+"x4")
|
|
1193
|
+
}
|
|
1194
|
+
if sixes := valueString(leader, "sixes"); sixes != "" {
|
|
1195
|
+
extras = append(extras, sixes+"x6")
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
if role == "bowling" {
|
|
1199
|
+
if overs := valueString(leader, "overs"); overs != "" {
|
|
1200
|
+
extras = append(extras, overs+" ov")
|
|
1201
|
+
}
|
|
1202
|
+
if runs := valueString(leader, "runs"); runs != "" {
|
|
1203
|
+
extras = append(extras, runs+" runs")
|
|
1204
|
+
}
|
|
1205
|
+
if economy := valueString(leader, "economyRate"); economy != "" {
|
|
1206
|
+
extras = append(extras, "econ "+economy)
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
row := joinParts(player, score, strings.Join(extras, ", "))
|
|
1211
|
+
if row == "" {
|
|
1212
|
+
continue
|
|
1213
|
+
}
|
|
1214
|
+
lines = append(lines, fmt.Sprintf(" %d. %s", idx+1, row))
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
return lines
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
func leaderCategoryRole(category map[string]any) string {
|
|
1221
|
+
name := strings.ToLower(strings.TrimSpace(firstNonEmpty(valueString(category, "name"), valueString(category, "displayName"))))
|
|
1222
|
+
if strings.Contains(name, "run") || strings.Contains(name, "bat") {
|
|
1223
|
+
return "batting"
|
|
1224
|
+
}
|
|
1225
|
+
if strings.Contains(name, "wicket") || strings.Contains(name, "bowl") {
|
|
1226
|
+
return "bowling"
|
|
1227
|
+
}
|
|
1228
|
+
return "other"
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
func analysisRowLabel(row map[string]any) string {
|
|
1232
|
+
if row == nil {
|
|
1233
|
+
return ""
|
|
1234
|
+
}
|
|
1235
|
+
parts := make([]string, 0, 4)
|
|
1236
|
+
if player := valueString(row, "playerName"); player != "" {
|
|
1237
|
+
parts = append(parts, player)
|
|
1238
|
+
}
|
|
1239
|
+
if team := valueString(row, "teamName"); team != "" {
|
|
1240
|
+
parts = append(parts, team)
|
|
1241
|
+
}
|
|
1242
|
+
if dismissal := valueString(row, "dismissalType"); dismissal != "" {
|
|
1243
|
+
parts = append(parts, dismissal)
|
|
1244
|
+
}
|
|
1245
|
+
innings := valueString(row, "inningsNumber")
|
|
1246
|
+
period := valueString(row, "period")
|
|
1247
|
+
if innings != "" && innings != "0" {
|
|
1248
|
+
if period != "" && period != "0" {
|
|
1249
|
+
parts = append(parts, innings+"/"+period)
|
|
1250
|
+
} else {
|
|
1251
|
+
parts = append(parts, innings)
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
if len(parts) > 0 {
|
|
1255
|
+
return strings.Join(parts, " | ")
|
|
1256
|
+
}
|
|
1257
|
+
if key := valueString(row, "key"); key != "" {
|
|
1258
|
+
return sanitizeAnalysisKey(key)
|
|
1259
|
+
}
|
|
1260
|
+
return "row"
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
func sanitizeAnalysisKey(key string) string {
|
|
1264
|
+
key = strings.TrimSpace(key)
|
|
1265
|
+
if key == "" {
|
|
1266
|
+
return ""
|
|
1267
|
+
}
|
|
1268
|
+
segments := strings.Split(key, "|")
|
|
1269
|
+
values := make([]string, 0, len(segments))
|
|
1270
|
+
for _, segment := range segments {
|
|
1271
|
+
segment = strings.TrimSpace(segment)
|
|
1272
|
+
if segment == "" {
|
|
1273
|
+
continue
|
|
1274
|
+
}
|
|
1275
|
+
_, value, ok := strings.Cut(segment, "=")
|
|
1276
|
+
if !ok {
|
|
1277
|
+
continue
|
|
1278
|
+
}
|
|
1279
|
+
value = sanitizeWarningText(value)
|
|
1280
|
+
if value == "" || looksLikeRawIdentifier(value) {
|
|
1281
|
+
continue
|
|
1282
|
+
}
|
|
1283
|
+
values = append(values, value)
|
|
1284
|
+
}
|
|
1285
|
+
if len(values) == 0 {
|
|
1286
|
+
return "row"
|
|
1287
|
+
}
|
|
1288
|
+
return strings.Join(values, " | ")
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
func looksLikeRawIdentifier(value string) bool {
|
|
1292
|
+
value = strings.TrimSpace(strings.ToLower(value))
|
|
1293
|
+
if value == "" {
|
|
1294
|
+
return true
|
|
1295
|
+
}
|
|
1296
|
+
if strings.Contains(value, "/") || strings.Contains(value, "http://") || strings.Contains(value, "https://") {
|
|
1297
|
+
return true
|
|
1298
|
+
}
|
|
1299
|
+
for _, r := range value {
|
|
1300
|
+
if r < '0' || r > '9' {
|
|
1301
|
+
return false
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
return true
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
var (
|
|
1308
|
+
urlLikeTokenPattern = regexp.MustCompile(`https?://\S+`)
|
|
1309
|
+
htmlTagPattern = regexp.MustCompile(`<[^>]*>`)
|
|
1310
|
+
internalPathPattern = regexp.MustCompile(`(?:https?://[^\s]+)?/v2/sports/cricket[^\s]*`)
|
|
1311
|
+
spaceCollapsePattern = regexp.MustCompile(`\s+`)
|
|
1312
|
+
)
|
|
1313
|
+
|
|
1314
|
+
func sanitizeWarningsForText(warnings []string) []string {
|
|
1315
|
+
out := make([]string, 0, len(warnings))
|
|
1316
|
+
for _, warning := range warnings {
|
|
1317
|
+
cleaned := sanitizeWarningText(warning)
|
|
1318
|
+
if cleaned == "" {
|
|
1319
|
+
continue
|
|
1320
|
+
}
|
|
1321
|
+
out = append(out, cleaned)
|
|
1322
|
+
}
|
|
1323
|
+
if len(out) == 0 {
|
|
1324
|
+
return []string{"partial data"}
|
|
1325
|
+
}
|
|
1326
|
+
return out
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
func sanitizeWarningText(raw string) string {
|
|
1330
|
+
raw = strings.TrimSpace(raw)
|
|
1331
|
+
if raw == "" {
|
|
1332
|
+
return ""
|
|
1333
|
+
}
|
|
1334
|
+
cleaned := htmlTagPattern.ReplaceAllString(raw, " ")
|
|
1335
|
+
cleaned = internalPathPattern.ReplaceAllString(cleaned, " ")
|
|
1336
|
+
cleaned = urlLikeTokenPattern.ReplaceAllString(cleaned, " ")
|
|
1337
|
+
cleaned = strings.ReplaceAll(cleaned, `""`, "")
|
|
1338
|
+
lowered := strings.ToLower(cleaned)
|
|
1339
|
+
if strings.Contains(lowered, "backend fetch failed") || strings.Contains(lowered, "context deadline exceeded") {
|
|
1340
|
+
return "upstream request failed"
|
|
1341
|
+
}
|
|
1342
|
+
cleaned = spaceCollapsePattern.ReplaceAllString(cleaned, " ")
|
|
1343
|
+
return strings.TrimSpace(cleaned)
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
func overBallLabel(entity map[string]any) string {
|
|
1347
|
+
over := valueString(entity, "overNumber")
|
|
1348
|
+
ball := valueString(entity, "ballNumber")
|
|
1349
|
+
if over == "" {
|
|
1350
|
+
return ""
|
|
1351
|
+
}
|
|
1352
|
+
if ball == "" {
|
|
1353
|
+
return "over " + over
|
|
1354
|
+
}
|
|
1355
|
+
return "over " + over + "." + ball
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
func involvementLabel(entity map[string]any) string {
|
|
1359
|
+
roles := stringSliceValue(entity, "involvement")
|
|
1360
|
+
if len(roles) == 0 {
|
|
1361
|
+
return ""
|
|
1362
|
+
}
|
|
1363
|
+
if len(roles) > 1 {
|
|
1364
|
+
filtered := make([]string, 0, len(roles))
|
|
1365
|
+
for _, role := range roles {
|
|
1366
|
+
if role == "involved" {
|
|
1367
|
+
continue
|
|
1368
|
+
}
|
|
1369
|
+
filtered = append(filtered, role)
|
|
1370
|
+
}
|
|
1371
|
+
if len(filtered) > 0 {
|
|
1372
|
+
roles = filtered
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
return strings.Join(roles, ", ")
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
func sliceSummary(entity map[string]any, key string, limit int) string {
|
|
1379
|
+
return summarizeNamedItems(sliceValue(entity, key), limit)
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
func summarizeNamedItems(items []any, limit int) string {
|
|
1383
|
+
if len(items) == 0 {
|
|
1384
|
+
return ""
|
|
1385
|
+
}
|
|
1386
|
+
parts := make([]string, 0, minInt(len(items), limit))
|
|
1387
|
+
for _, raw := range items {
|
|
1388
|
+
mapped, ok := raw.(map[string]any)
|
|
1389
|
+
if !ok {
|
|
1390
|
+
continue
|
|
1391
|
+
}
|
|
1392
|
+
name := firstNonEmpty(
|
|
1393
|
+
valueString(mapped, "displayName"),
|
|
1394
|
+
valueString(mapped, "fullName"),
|
|
1395
|
+
valueString(mapped, "shortName"),
|
|
1396
|
+
valueString(mapped, "name"),
|
|
1397
|
+
valueString(mapped, "headline"),
|
|
1398
|
+
valueString(mapped, "title"),
|
|
1399
|
+
)
|
|
1400
|
+
if name == "" {
|
|
1401
|
+
name = namedValue(mapped)
|
|
1402
|
+
}
|
|
1403
|
+
if name == "" {
|
|
1404
|
+
continue
|
|
1405
|
+
}
|
|
1406
|
+
parts = append(parts, name)
|
|
1407
|
+
if len(parts) >= limit {
|
|
1408
|
+
break
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
if len(parts) == 0 {
|
|
1412
|
+
return fmt.Sprintf("%d items", len(items))
|
|
1413
|
+
}
|
|
1414
|
+
if len(items) > len(parts) {
|
|
1415
|
+
return strings.Join(parts, ", ") + fmt.Sprintf(" (+%d more)", len(items)-len(parts))
|
|
1416
|
+
}
|
|
1417
|
+
return strings.Join(parts, ", ")
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
func namedValue(raw any) string {
|
|
1421
|
+
mapped, ok := raw.(map[string]any)
|
|
1422
|
+
if !ok || mapped == nil {
|
|
1423
|
+
return ""
|
|
1424
|
+
}
|
|
1425
|
+
name := firstNonEmpty(
|
|
1426
|
+
valueString(mapped, "displayName"),
|
|
1427
|
+
valueString(mapped, "fullName"),
|
|
1428
|
+
valueString(mapped, "shortName"),
|
|
1429
|
+
valueString(mapped, "name"),
|
|
1430
|
+
)
|
|
1431
|
+
if name != "" {
|
|
1432
|
+
return name
|
|
1433
|
+
}
|
|
1434
|
+
id := valueString(mapped, "id")
|
|
1435
|
+
if looksLikeRawIdentifier(id) {
|
|
1436
|
+
return ""
|
|
1437
|
+
}
|
|
1438
|
+
return id
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
func styleSummary(entity map[string]any) string {
|
|
1442
|
+
items := sliceValue(entity, "styles")
|
|
1443
|
+
if len(items) == 0 {
|
|
1444
|
+
return ""
|
|
1445
|
+
}
|
|
1446
|
+
parts := make([]string, 0, len(items))
|
|
1447
|
+
for _, raw := range items {
|
|
1448
|
+
mapped, ok := raw.(map[string]any)
|
|
1449
|
+
if !ok {
|
|
1450
|
+
continue
|
|
1451
|
+
}
|
|
1452
|
+
name := firstNonEmpty(valueString(mapped, "description"), valueString(mapped, "shortDescription"), valueString(mapped, "type"))
|
|
1453
|
+
if name == "" {
|
|
1454
|
+
continue
|
|
1455
|
+
}
|
|
1456
|
+
parts = append(parts, name)
|
|
1457
|
+
}
|
|
1458
|
+
if len(parts) == 0 {
|
|
1459
|
+
return fmt.Sprintf("%d styles", len(items))
|
|
1460
|
+
}
|
|
1461
|
+
return strings.Join(parts, ", ")
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
func summarizePlayerMatchSummary(summary map[string]any) string {
|
|
1465
|
+
runs := valueString(summary, "runs")
|
|
1466
|
+
balls := valueString(summary, "ballsFaced")
|
|
1467
|
+
dismissal := valueString(summary, "dismissalName")
|
|
1468
|
+
strikeRate := valueString(summary, "strikeRate")
|
|
1469
|
+
parts := []string{
|
|
1470
|
+
nonEmptyRunsBalls(runs, balls),
|
|
1471
|
+
nonEmptyLabel("dismissal", dismissal),
|
|
1472
|
+
nonEmptyLabel("SR", strikeRate),
|
|
1473
|
+
nonEmptyLabel("econ", valueString(summary, "economyRate")),
|
|
1474
|
+
nonEmptyLabel("dots", valueString(summary, "dots")),
|
|
1475
|
+
}
|
|
1476
|
+
text := joinParts(parts...)
|
|
1477
|
+
if text == "" {
|
|
1478
|
+
return ""
|
|
1479
|
+
}
|
|
1480
|
+
return "Summary: " + text
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
func nonEmptyRunsBalls(runs, balls string) string {
|
|
1484
|
+
if runs == "" && balls == "" {
|
|
1485
|
+
return ""
|
|
1486
|
+
}
|
|
1487
|
+
if runs != "" && balls != "" {
|
|
1488
|
+
return runs + " runs off " + balls
|
|
1489
|
+
}
|
|
1490
|
+
if runs != "" {
|
|
1491
|
+
return runs + " runs"
|
|
1492
|
+
}
|
|
1493
|
+
return balls + " balls"
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
func nonEmptyLabel(label, value string) string {
|
|
1497
|
+
value = strings.TrimSpace(value)
|
|
1498
|
+
if value == "" {
|
|
1499
|
+
return ""
|
|
1500
|
+
}
|
|
1501
|
+
return label + " " + value
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
func mapsKeys(m map[string]any) []string {
|
|
1505
|
+
keys := make([]string, 0, len(m))
|
|
1506
|
+
for key := range m {
|
|
1507
|
+
keys = append(keys, key)
|
|
1508
|
+
}
|
|
1509
|
+
return keys
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
func valueString(m map[string]any, key string) string {
|
|
1513
|
+
value, ok := m[key]
|
|
1514
|
+
if !ok || value == nil {
|
|
1515
|
+
return ""
|
|
1516
|
+
}
|
|
1517
|
+
switch typed := value.(type) {
|
|
1518
|
+
case string:
|
|
1519
|
+
return strings.TrimSpace(typed)
|
|
1520
|
+
case float64:
|
|
1521
|
+
if typed == float64(int64(typed)) {
|
|
1522
|
+
return fmt.Sprintf("%d", int64(typed))
|
|
1523
|
+
}
|
|
1524
|
+
return fmt.Sprintf("%.2f", typed)
|
|
1525
|
+
case bool:
|
|
1526
|
+
return fmt.Sprintf("%t", typed)
|
|
1527
|
+
case map[string]any:
|
|
1528
|
+
return firstNonEmpty(
|
|
1529
|
+
valueString(typed, "displayName"),
|
|
1530
|
+
valueString(typed, "fullName"),
|
|
1531
|
+
valueString(typed, "shortName"),
|
|
1532
|
+
valueString(typed, "name"),
|
|
1533
|
+
valueString(typed, "headline"),
|
|
1534
|
+
valueString(typed, "title"),
|
|
1535
|
+
valueString(typed, "id"),
|
|
1536
|
+
)
|
|
1537
|
+
case []any:
|
|
1538
|
+
return fmt.Sprintf("%d items", len(typed))
|
|
1539
|
+
default:
|
|
1540
|
+
return strings.TrimSpace(fmt.Sprintf("%v", typed))
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
func sliceValue(m map[string]any, key string) []any {
|
|
1545
|
+
value, ok := m[key]
|
|
1546
|
+
if !ok || value == nil {
|
|
1547
|
+
return nil
|
|
1548
|
+
}
|
|
1549
|
+
raw, ok := value.([]any)
|
|
1550
|
+
if !ok {
|
|
1551
|
+
return nil
|
|
1552
|
+
}
|
|
1553
|
+
return raw
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
func stringSliceValue(m map[string]any, key string) []string {
|
|
1557
|
+
raw := sliceValue(m, key)
|
|
1558
|
+
if len(raw) == 0 {
|
|
1559
|
+
return nil
|
|
1560
|
+
}
|
|
1561
|
+
out := make([]string, 0, len(raw))
|
|
1562
|
+
for _, item := range raw {
|
|
1563
|
+
asString, ok := item.(string)
|
|
1564
|
+
if !ok {
|
|
1565
|
+
continue
|
|
1566
|
+
}
|
|
1567
|
+
asString = strings.TrimSpace(asString)
|
|
1568
|
+
if asString == "" {
|
|
1569
|
+
continue
|
|
1570
|
+
}
|
|
1571
|
+
out = append(out, asString)
|
|
1572
|
+
}
|
|
1573
|
+
return out
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
func printableValue(value any) string {
|
|
1577
|
+
switch typed := value.(type) {
|
|
1578
|
+
case string:
|
|
1579
|
+
return typed
|
|
1580
|
+
case float64:
|
|
1581
|
+
if typed == float64(int64(typed)) {
|
|
1582
|
+
return fmt.Sprintf("%d", int64(typed))
|
|
1583
|
+
}
|
|
1584
|
+
return fmt.Sprintf("%.2f", typed)
|
|
1585
|
+
case []any:
|
|
1586
|
+
return fmt.Sprintf("%d items", len(typed))
|
|
1587
|
+
case map[string]any:
|
|
1588
|
+
keys := mapsKeys(typed)
|
|
1589
|
+
sort.Strings(keys)
|
|
1590
|
+
if len(keys) > 5 {
|
|
1591
|
+
keys = keys[:5]
|
|
1592
|
+
}
|
|
1593
|
+
return "{" + strings.Join(keys, ", ") + "}"
|
|
1594
|
+
default:
|
|
1595
|
+
return fmt.Sprintf("%v", typed)
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
func isEmptyValue(value any) bool {
|
|
1600
|
+
if value == nil {
|
|
1601
|
+
return true
|
|
1602
|
+
}
|
|
1603
|
+
switch typed := value.(type) {
|
|
1604
|
+
case string:
|
|
1605
|
+
return strings.TrimSpace(typed) == ""
|
|
1606
|
+
case []any:
|
|
1607
|
+
return len(typed) == 0
|
|
1608
|
+
case map[string]any:
|
|
1609
|
+
return len(typed) == 0
|
|
1610
|
+
default:
|
|
1611
|
+
return false
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
func joinParts(parts ...string) string {
|
|
1616
|
+
trimmed := make([]string, 0, len(parts))
|
|
1617
|
+
for _, part := range parts {
|
|
1618
|
+
part = strings.TrimSpace(part)
|
|
1619
|
+
if part == "" {
|
|
1620
|
+
continue
|
|
1621
|
+
}
|
|
1622
|
+
trimmed = append(trimmed, part)
|
|
1623
|
+
}
|
|
1624
|
+
return strings.Join(trimmed, " - ")
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
func firstNonEmpty(values ...string) string {
|
|
1628
|
+
for _, value := range values {
|
|
1629
|
+
value = strings.TrimSpace(value)
|
|
1630
|
+
if value != "" {
|
|
1631
|
+
return value
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
return ""
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
func bracket(value string) string {
|
|
1638
|
+
value = strings.TrimSpace(value)
|
|
1639
|
+
if value == "" {
|
|
1640
|
+
return ""
|
|
1641
|
+
}
|
|
1642
|
+
return "(" + value + ")"
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
func titleize(value string) string {
|
|
1646
|
+
value = strings.ReplaceAll(value, "_", " ")
|
|
1647
|
+
parts := strings.Fields(value)
|
|
1648
|
+
for i := range parts {
|
|
1649
|
+
parts[i] = strings.Title(parts[i])
|
|
1650
|
+
}
|
|
1651
|
+
return strings.Join(parts, " ")
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
func sentenceCase(value string) string {
|
|
1655
|
+
value = strings.TrimSpace(value)
|
|
1656
|
+
if value == "" {
|
|
1657
|
+
return ""
|
|
1658
|
+
}
|
|
1659
|
+
value = strings.TrimSuffix(value, ".")
|
|
1660
|
+
if len(value) == 1 {
|
|
1661
|
+
return strings.ToUpper(value) + "."
|
|
1662
|
+
}
|
|
1663
|
+
return strings.ToUpper(value[:1]) + value[1:] + "."
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
func matchTeamsLabel(entity map[string]any) string {
|
|
1667
|
+
teams := sliceValue(entity, "teams")
|
|
1668
|
+
if len(teams) == 0 {
|
|
1669
|
+
return ""
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
parts := make([]string, 0, len(teams))
|
|
1673
|
+
for _, raw := range teams {
|
|
1674
|
+
team, ok := raw.(map[string]any)
|
|
1675
|
+
if !ok {
|
|
1676
|
+
continue
|
|
1677
|
+
}
|
|
1678
|
+
name := firstNonEmpty(valueString(team, "shortName"), valueString(team, "name"), valueString(team, "id"))
|
|
1679
|
+
if name == "" {
|
|
1680
|
+
continue
|
|
1681
|
+
}
|
|
1682
|
+
parts = append(parts, name)
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
if len(parts) == 0 {
|
|
1686
|
+
return ""
|
|
1687
|
+
}
|
|
1688
|
+
return strings.Join(parts, " vs ")
|
|
1689
|
+
}
|