Skip to content

Commit 3c7ca4d

Browse files
Add FE schema and parser for cricket match data (#13733)
* Create cricket match schema parser - move DCR types to parsing file - add test - add fixture data - add FE schema - add validation function - remove "Data" suffix from types
1 parent 29361a6 commit 3c7ca4d

File tree

9 files changed

+2824
-59
lines changed

9 files changed

+2824
-59
lines changed

dotcom-rendering/fixtures/generated/cricket-match.ts

Lines changed: 1625 additions & 0 deletions
Large diffs are not rendered by default.

dotcom-rendering/scripts/jsonSchema/schema.mjs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// @ts-check
2-
import path from 'node:path';
32
import fs from 'node:fs';
4-
import TJS from 'typescript-json-schema';
3+
import path from 'node:path';
54
import { fileURLToPath } from 'node:url';
5+
import TJS from 'typescript-json-schema';
66

77
const __dirname = path.dirname(fileURLToPath(import.meta.url));
88
const root = path.resolve(__dirname, '..', '..');
@@ -15,6 +15,7 @@ const program = TJS.getProgramFromFiles(
1515
path.resolve(`${root}/src/frontend/feTagPage.ts`),
1616
path.resolve(`${root}/src/types/newslettersPage.ts`),
1717
path.resolve(`${root}/src/types/editionsCrossword.ts`),
18+
path.resolve(`${root}/src/frontend/feCricketMatchPage.ts`),
1819
path.resolve(`${root}/src/frontend/feFootballMatchListPage.ts`),
1920
path.resolve(`${root}/src/frontend/feFootballTablesPage.ts`),
2021
],
@@ -67,6 +68,10 @@ const schemas = [
6768
typeName: 'FEFootballTablesPage',
6869
file: `${root}/src/frontend/schemas/feFootballTablesPage.json`,
6970
},
71+
{
72+
typeName: 'FECricketMatchPage',
73+
file: `${root}/src/frontend/schemas/feCricketMatchPage.json`,
74+
},
7075
];
7176

7277
/**

dotcom-rendering/scripts/test-data/gen-fixtures.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const { config } = require('../../fixtures/config');
88
const { configOverrides } = require('../../fixtures/config-overrides');
99
const { switchOverrides } = require('../../fixtures/switch-overrides');
1010
const {
11+
validateAsCricketMatchPageType,
1112
validateAsFEArticle,
1213
validateAsFootballMatchListPage,
1314
} = require('../../src/model/validate');
@@ -356,6 +357,48 @@ requests.push(
356357
}),
357358
);
358359

360+
requests.push(
361+
// This match data will expire after two months, find a new match if this needs updating
362+
fetch(
363+
'https://www.theguardian.com/sport/cricket/match/2025-03-26/australia-women-s-cricket-team.json?dcr',
364+
)
365+
.then((res) => res.json())
366+
.then((json) => {
367+
// These configs are returning from frontend
368+
// but are not necessary for cricket pages
369+
delete json.config.hasLiveBlogTopAd;
370+
delete json.config.userAttributesApiUrl;
371+
delete json.config.weatherapiurl;
372+
delete json.config.isAdFree;
373+
delete json.config.userBenefitsApiUrl;
374+
375+
const cricketMatchData = validateAsCricketMatchPageType(json);
376+
377+
// Write the new frontend fixture data
378+
const contents = `${HEADER}
379+
import type { FECricketMatchPage } from '../../src/frontend/feCricketMatch';
380+
381+
export const cricketMatchData: FECricketMatchPage = ${JSON.stringify(
382+
cricketMatchData,
383+
null,
384+
4,
385+
)}
386+
`;
387+
388+
return fs.writeFile(
389+
`${root}/fixtures/generated/cricket-match.ts`,
390+
contents,
391+
'utf8',
392+
);
393+
})
394+
.then(() => 'cricket-match.ts')
395+
.catch((err) => {
396+
throw new Error('Failed to create cricket-match.ts', {
397+
cause: err,
398+
});
399+
}),
400+
);
401+
359402
Promise.allSettled(requests)
360403
.then((results) => {
361404
const successful = results.filter(

dotcom-rendering/src/components/CricketScorecard.tsx

Lines changed: 13 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@ import {
1111
} from '@guardian/source/foundations';
1212
import { Stack } from '@guardian/source/react-components';
1313
import type { ReactNode } from 'react';
14+
import type {
15+
Batter,
16+
Bowler,
17+
CricketTeam,
18+
Extras,
19+
FallOfWicket,
20+
Innings,
21+
InningsTotals,
22+
} from '../cricketMatch';
1423
import { palette } from '../palette';
1524

1625
const borderStyle = css`
@@ -108,60 +117,7 @@ const getExtrasDescription = ({
108117
</>
109118
);
110119

111-
type BowlerData = {
112-
name: string;
113-
overs: number;
114-
maidens: number;
115-
runs: number;
116-
wickets: number;
117-
balls: number;
118-
};
119-
120-
type BatterData = {
121-
name: string;
122-
ballsFaced: number;
123-
runs: number;
124-
fours: number;
125-
sixes: number;
126-
howOut: string;
127-
};
128-
129-
type Extras = {
130-
byes: number;
131-
legByes: number;
132-
noBalls: number;
133-
penalties: number;
134-
wides: number;
135-
};
136-
137-
type InningsTotals = {
138-
runs: number;
139-
overs: string;
140-
wickets: number;
141-
extras: number;
142-
};
143-
144-
type FallOfWicketData = {
145-
order: number;
146-
name: string;
147-
runs: number;
148-
};
149-
150-
type CricketTeam = {
151-
name: string;
152-
lineup: string[];
153-
};
154-
155-
type InningsData = {
156-
description: string;
157-
bowlers: BowlerData[];
158-
batters: BatterData[];
159-
extras: Extras;
160-
inningsTotals: InningsTotals;
161-
fallOfWickets: FallOfWicketData[];
162-
};
163-
164-
const Bowling = ({ bowlers }: { bowlers: BowlerData[] }) => (
120+
const Bowling = ({ bowlers }: { bowlers: Bowler[] }) => (
165121
<table css={tableStyles}>
166122
<thead>
167123
<tr>
@@ -195,7 +151,7 @@ const Batting = ({
195151
extras,
196152
inningsTotals,
197153
}: {
198-
batters: BatterData[];
154+
batters: Batter[];
199155
extras: Extras;
200156
inningsTotals: InningsTotals;
201157
}) => (
@@ -284,7 +240,7 @@ const Batting = ({
284240
const FallOfWickets = ({
285241
fallOfWickets,
286242
}: {
287-
fallOfWickets: FallOfWicketData[];
243+
fallOfWickets: FallOfWicket[];
288244
}) => (
289245
<table css={tableStyles}>
290246
<thead>
@@ -307,7 +263,7 @@ const FallOfWickets = ({
307263
);
308264

309265
type Props = {
310-
allInnings: InningsData[];
266+
allInnings: Innings[];
311267
officials: string[];
312268
homeTeam: CricketTeam;
313269
awayTeam: CricketTeam;
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { cricketMatchData } from '../fixtures/generated/cricket-match';
2+
import { parse } from './cricketMatch';
3+
import type { FECricketMatch } from './frontend/feCricketMatchPage';
4+
import { okOrThrow } from './lib/result';
5+
6+
describe('parse', () => {
7+
it('parses cricket match correctly', () => {
8+
const parsedResult = okOrThrow(
9+
parse(cricketMatchData.cricketMatch),
10+
'Expected parsing cricket match to succeed',
11+
);
12+
expect(parsedResult.awayTeam.lineup.length).toBe(11);
13+
expect(parsedResult.innings.length).toBe(2);
14+
});
15+
16+
it('reverses the innings for display', () => {
17+
const parsedResult = okOrThrow(
18+
parse(cricketMatchData.cricketMatch),
19+
'Expected parsing cricket match to succeed',
20+
);
21+
22+
expect(parsedResult.innings[1]?.description).toBe(
23+
cricketMatchData.cricketMatch.innings[0]?.description,
24+
);
25+
26+
expect(parsedResult.innings[0]?.fallOfWickets.length).toBe(8);
27+
expect(parsedResult.innings[0]?.inningsTotals.wickets).toBe(8);
28+
});
29+
30+
it('calculates the number of wickets fallen', () => {
31+
const parsedResult = okOrThrow(
32+
parse(cricketMatchData.cricketMatch),
33+
'Expected parsing cricket match to succeed',
34+
);
35+
36+
expect(parsedResult.innings[0]?.fallOfWickets.length).toBe(8);
37+
expect(parsedResult.innings[0]?.inningsTotals.wickets).toBe(8);
38+
});
39+
40+
it('returns an error if home and away team cannot be determined', () => {
41+
const cricketMatch: FECricketMatch = {
42+
...cricketMatchData.cricketMatch,
43+
teams: [
44+
{
45+
name: '',
46+
lineup: [],
47+
home: true,
48+
id: '',
49+
},
50+
{
51+
name: '',
52+
lineup: [],
53+
home: true,
54+
id: '2',
55+
},
56+
],
57+
};
58+
59+
const parsedResult = parse(cricketMatch);
60+
expect(parsedResult.kind).toBe('error');
61+
});
62+
});

dotcom-rendering/src/cricketMatch.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { isUndefined } from '@guardian/libs';
2+
import type {
3+
FECricketInnings,
4+
FECricketMatch,
5+
} from './frontend/feCricketMatchPage';
6+
import type { FEFootballPageConfig } from './frontend/feFootballDataPage';
7+
import type { EditionId } from './lib/edition';
8+
import { error, ok, type Result } from './lib/result';
9+
import type { NavType } from './model/extract-nav';
10+
import type { FooterType } from './types/footer';
11+
12+
export type Bowler = {
13+
name: string;
14+
overs: number;
15+
maidens: number;
16+
runs: number;
17+
wickets: number;
18+
balls: number;
19+
};
20+
21+
export type Batter = {
22+
name: string;
23+
ballsFaced: number;
24+
runs: number;
25+
fours: number;
26+
sixes: number;
27+
howOut: string;
28+
};
29+
30+
export type Extras = {
31+
byes: number;
32+
legByes: number;
33+
noBalls: number;
34+
penalties: number;
35+
wides: number;
36+
};
37+
38+
export type InningsTotals = {
39+
runs: number;
40+
overs: string;
41+
wickets: number;
42+
extras: number;
43+
};
44+
45+
export type FallOfWicket = {
46+
order: number;
47+
name: string;
48+
runs: number;
49+
};
50+
51+
export type CricketTeam = {
52+
name: string;
53+
lineup: string[];
54+
};
55+
56+
export type Innings = {
57+
description: string;
58+
bowlers: Bowler[];
59+
batters: Batter[];
60+
extras: Extras;
61+
inningsTotals: InningsTotals;
62+
fallOfWickets: FallOfWicket[];
63+
};
64+
65+
type CricketMatch = {
66+
homeTeam: CricketTeam;
67+
awayTeam: CricketTeam;
68+
officials: string[];
69+
innings: Innings[];
70+
};
71+
72+
const feInningsToDCARInnings = (feInnings: FECricketInnings): Innings => {
73+
const inningsTotals = {
74+
runs: feInnings.runsScored,
75+
overs: feInnings.overs,
76+
wickets: feInnings.fallOfWicket.length,
77+
extras: feInnings.extras,
78+
};
79+
80+
const extras = {
81+
byes: feInnings.byes,
82+
legByes: feInnings.legByes,
83+
noBalls: feInnings.noBalls,
84+
penalties: feInnings.penalties,
85+
wides: feInnings.wides,
86+
};
87+
88+
return {
89+
description: feInnings.description,
90+
inningsTotals,
91+
extras,
92+
fallOfWickets: feInnings.fallOfWicket,
93+
batters: feInnings.batters,
94+
bowlers: feInnings.bowlers,
95+
};
96+
};
97+
98+
export const parse = (
99+
feCricketMatch: FECricketMatch,
100+
): Result<string, CricketMatch> => {
101+
const homeTeam = feCricketMatch.teams.find((team) => team.home);
102+
const awayTeam = feCricketMatch.teams.find((team) => !team.home);
103+
104+
if (isUndefined(homeTeam) || isUndefined(awayTeam)) {
105+
return error('Could not determine home and away cricket teams');
106+
}
107+
108+
const innings = feCricketMatch.innings
109+
.map(feInningsToDCARInnings)
110+
.reverse();
111+
112+
return ok({
113+
homeTeam,
114+
awayTeam,
115+
officials: feCricketMatch.officials,
116+
innings,
117+
});
118+
};
119+
120+
export type CricketMatchPage = {
121+
match: CricketMatch;
122+
nav: NavType;
123+
editionId: EditionId;
124+
guardianBaseURL: string;
125+
config: FEFootballPageConfig;
126+
pageFooter: FooterType;
127+
isAdFreeUser: boolean;
128+
canonicalUrl?: string;
129+
contributionsServiceUrl: string;
130+
};

0 commit comments

Comments
 (0)