Skip to content

Commit d071e98

Browse files
c-wdelucis
andauthored
feat: Add game method to validate setup data (#831)
Add a `validateSetupData` method to the game object, which — if provided — is called by the API server when creating a match to check that the `setupData` sent to the server is valid. Co-authored-by: Chris Swithinbank <[email protected]>
1 parent 9fcbed7 commit d071e98

4 files changed

Lines changed: 61 additions & 2 deletions

File tree

docs/documentation/api/Game.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@
1010
// passed through the Game Creation API.
1111
setup: (ctx, setupData) => G,
1212

13+
// Optional function to validate the setupData before
14+
// matches are created. If this returns a value,
15+
// an error will be reported to the user and match
16+
// creation is aborted.
17+
validateSetupData: (setupData, numPlayers) => 'setupData is not valid!',
18+
1319
moves: {
1420
// short-form move.
1521
A: (G, ctx) => {},

src/server/api.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,19 @@ describe('.createRouter', () => {
102102
}
103103
: {},
104104
},
105+
{
106+
name: 'validate',
107+
setup: (_, setupData) =>
108+
setupData
109+
? {
110+
numTokens: setupData.tokens,
111+
}
112+
: {},
113+
validateSetupData: (setupData, numPlayers) =>
114+
numPlayers == 2 && setupData.tokens !== 2
115+
? 'Two player games must use two tokens'
116+
: undefined,
117+
},
105118
];
106119
});
107120

@@ -226,6 +239,34 @@ describe('.createRouter', () => {
226239
});
227240
});
228241

242+
describe('with setupData validation', () => {
243+
test('creates game if validation passes', async () => {
244+
response = await request(app.callback())
245+
.post('/games/validate/create')
246+
.send({
247+
numPlayers: 2,
248+
setupData: {
249+
tokens: 2,
250+
},
251+
});
252+
253+
expect(response.status).toEqual(200);
254+
});
255+
256+
test('returns error if validation fails', async () => {
257+
response = await request(app.callback())
258+
.post('/games/validate/create')
259+
.send({
260+
numPlayers: 2,
261+
setupData: {
262+
tokens: 3,
263+
},
264+
});
265+
266+
expect(response.status).toEqual(400);
267+
});
268+
});
269+
229270
describe('with unlisted option', () => {
230271
beforeEach(async () => {
231272
response = await request(app.callback())

src/server/api.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ export const createRouter = ({
124124
const game = games.find(g => g.name === gameName);
125125
if (!game) ctx.throw(404, 'Game ' + gameName + ' not found');
126126

127+
const setupDataError =
128+
game.validateSetupData && game.validateSetupData(setupData, numPlayers);
129+
if (setupDataError !== undefined) ctx.throw(400, setupDataError);
130+
127131
const matchID = await CreateMatch({
128132
db,
129133
game,

src/types.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,13 +231,21 @@ interface PhaseMap<G extends any = any, CtxWithPlugins extends Ctx = Ctx> {
231231
[phaseName: string]: PhaseConfig<G, CtxWithPlugins>;
232232
}
233233

234-
export interface Game<G extends any = any, CtxWithPlugins extends Ctx = Ctx> {
234+
export interface Game<
235+
G extends any = any,
236+
CtxWithPlugins extends Ctx = Ctx,
237+
SetupData extends any = any
238+
> {
235239
name?: string;
236240
minPlayers?: number;
237241
maxPlayers?: number;
238242
disableUndo?: boolean;
239243
seed?: string | number;
240-
setup?: (ctx: CtxWithPlugins, setupData?: any) => any;
244+
setup?: (ctx: CtxWithPlugins, setupData?: SetupData) => any;
245+
validateSetupData?: (
246+
setupData: SetupData | undefined,
247+
numPlayers: number
248+
) => string | undefined;
241249
moves?: MoveMap<G, CtxWithPlugins>;
242250
phases?: PhaseMap<G, CtxWithPlugins>;
243251
turn?: TurnConfig<G, CtxWithPlugins>;

0 commit comments

Comments
 (0)