Skip to content

Commit

Permalink
feat: use zod for all type definitions
Browse files Browse the repository at this point in the history
  • Loading branch information
LukeSchlangen committed Aug 21, 2023
1 parent 44e1822 commit 398a88b
Show file tree
Hide file tree
Showing 15 changed files with 98 additions and 132 deletions.
15 changes: 11 additions & 4 deletions app-dev/party-game/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
{
"extends": ["eslint:recommended", "next", "prettier", "google"],
"extends": [
"eslint:recommended",
"next",
"prettier",
"google"
],
"rules": {
"max-len": "off",
"require-jsdoc": "off",
"no-undef": "off"
"require-jsdoc": "off"
},
"globals": {
"React": true
}
}
}
6 changes: 3 additions & 3 deletions app-dev/party-game/app/api/create-game/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {Question, gameStates} from '@/app/types';
import {QueryDocumentSnapshot, Timestamp} from 'firebase-admin/firestore';
import {NextRequest, NextResponse} from 'next/server';
import {authenticationFailedResponse} from '@/app/lib/authentication-failed-response';
import {GameSettings} from '@/app/types/zod-types';
import {GameSettingsSchema} from '@/app/types';
import {badRequestResponse} from '@/app/lib/bad-request-response';

export async function POST(request: NextRequest) {
Expand All @@ -35,9 +35,9 @@ export async function POST(request: NextRequest) {

// Validate request
const body = await request.json();
const errorMessage = unknownValidator(body, GameSettings);
const errorMessage = unknownValidator(body, GameSettingsSchema);
if (errorMessage) return badRequestResponse({errorMessage});
const {timePerQuestion, timePerAnswer} = unknownParser(body, GameSettings);
const {timePerQuestion, timePerAnswer} = unknownParser(body, GameSettingsSchema);


const querySnapshot = await questionsRef.get();
Expand Down
6 changes: 3 additions & 3 deletions app-dev/party-game/app/api/delete-game/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {unknownParser, unknownValidator} from '@/app/lib/zod-parser';
import {gamesRef} from '@/app/lib/firebase-server-initialization';
import {getAuthenticatedUser} from '@/app/lib/server-side-auth';
import {NextRequest, NextResponse} from 'next/server';
import {GameIdObject} from '@/app/types/zod-types';
import {GameIdObjectSchema} from '@/app/types';
import {badRequestResponse} from '@/app/lib/bad-request-response';
import {authenticationFailedResponse} from '@/app/lib/authentication-failed-response';

Expand All @@ -33,9 +33,9 @@ export async function POST(request: NextRequest) {

// Validate request
const body = await request.json();
const errorMessage = unknownValidator(body, GameIdObject);
const errorMessage = unknownValidator(body, GameIdObjectSchema);
if (errorMessage) return badRequestResponse({errorMessage});
const {gameId} = unknownParser(body, GameIdObject);
const {gameId} = unknownParser(body, GameIdObjectSchema);

const gameRef = await gamesRef.doc(gameId);
const gameDoc = await gameRef.get();
Expand Down
6 changes: 3 additions & 3 deletions app-dev/party-game/app/api/exit-game/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {getAuthenticatedUser} from '@/app/lib/server-side-auth';
import {FieldValue} from 'firebase-admin/firestore';
import {NextRequest, NextResponse} from 'next/server';
import {badRequestResponse} from '@/app/lib/bad-request-response';
import {GameIdObject} from '@/app/types/zod-types';
import {GameIdObjectSchema} from '@/app/types';
import {authenticationFailedResponse} from '@/app/lib/authentication-failed-response';

export async function POST(request: NextRequest) {
Expand All @@ -33,9 +33,9 @@ export async function POST(request: NextRequest) {

// Validate request
const body = await request.json();
const errorMessage = unknownValidator(body, GameIdObject);
const errorMessage = unknownValidator(body, GameIdObjectSchema);
if (errorMessage) return badRequestResponse({errorMessage});
const {gameId} = unknownParser(body, GameIdObject);
const {gameId} = unknownParser(body, GameIdObjectSchema);
const gameRef = await gamesRef.doc(gameId);

// update database to exit the game
Expand Down
6 changes: 3 additions & 3 deletions app-dev/party-game/app/api/join-game/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {generateName} from '@/app/lib/name-generator';
import {getAuthenticatedUser} from '@/app/lib/server-side-auth';
import {NextRequest, NextResponse} from 'next/server';
import {badRequestResponse} from '@/app/lib/bad-request-response';
import {GameIdObject} from '@/app/types/zod-types';
import {GameIdObjectSchema} from '@/app/types';
import {authenticationFailedResponse} from '@/app/lib/authentication-failed-response';

export async function POST(request: NextRequest) {
Expand All @@ -34,9 +34,9 @@ export async function POST(request: NextRequest) {

// Validate request
const body = await request.json();
const errorMessage = unknownValidator(body, GameIdObject);
const errorMessage = unknownValidator(body, GameIdObjectSchema);
if (errorMessage) return badRequestResponse({errorMessage});
const {gameId} = unknownParser(body, GameIdObject);
const {gameId} = unknownParser(body, GameIdObjectSchema);
const gameRef = await gamesRef.doc(gameId);

// update database to join the game
Expand Down
6 changes: 3 additions & 3 deletions app-dev/party-game/app/api/nudge-game/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ import {gameStates} from '@/app/types';
import {Timestamp} from 'firebase-admin/firestore';
import {NextRequest, NextResponse} from 'next/server';
import {badRequestResponse} from '@/app/lib/bad-request-response';
import {GameIdObject} from '@/app/types/zod-types';
import {GameIdObjectSchema} from '@/app/types';

export async function POST(request: NextRequest) {
// Validate request
const body = await request.json();
const errorMessage = unknownValidator(body, GameIdObject);
const errorMessage = unknownValidator(body, GameIdObjectSchema);
if (errorMessage) return badRequestResponse({errorMessage});
const {gameId} = unknownParser(body, GameIdObject);
const {gameId} = unknownParser(body, GameIdObjectSchema);

const gameRef = await gamesRef.doc(gameId);
const gameDoc = await gameRef.get();
Expand Down
7 changes: 3 additions & 4 deletions app-dev/party-game/app/api/start-game/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,10 @@
import {unknownParser, unknownValidator} from '@/app/lib/zod-parser';
import {gamesRef} from '@/app/lib/firebase-server-initialization';
import {getAuthenticatedUser} from '@/app/lib/server-side-auth';
import {gameStates} from '@/app/types';
import {GameIdObjectSchema, gameStates} from '@/app/types';
import {FieldValue} from 'firebase-admin/firestore';
import {NextRequest, NextResponse} from 'next/server';
import {badRequestResponse} from '@/app/lib/bad-request-response';
import {GameIdObject} from '@/app/types/zod-types';
import {authenticationFailedResponse} from '@/app/lib/authentication-failed-response';

export async function POST(request: NextRequest) {
Expand All @@ -35,9 +34,9 @@ export async function POST(request: NextRequest) {

// Validate request
const body = await request.json();
const errorMessage = unknownValidator(body, GameIdObject);
const errorMessage = unknownValidator(body, GameIdObjectSchema);
if (errorMessage) return badRequestResponse({errorMessage});
const {gameId} = unknownParser(body, GameIdObject);
const {gameId} = unknownParser(body, GameIdObjectSchema);

const gameRef = await gamesRef.doc(gameId);
const gameDoc = await gameRef.get();
Expand Down
6 changes: 3 additions & 3 deletions app-dev/party-game/app/api/update-answer/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {gamesRef} from '@/app/lib/firebase-server-initialization';
import {getAuthenticatedUser} from '@/app/lib/server-side-auth';
import {gameStates} from '@/app/types';
import {NextRequest, NextResponse} from 'next/server';
import {AnswerSelectionWithGameId} from '@/app/types/zod-types';
import {AnswerSelectionWithGameIdSchema} from '@/app/types';
import {badRequestResponse} from '@/app/lib/bad-request-response';
import {authenticationFailedResponse} from '@/app/lib/authentication-failed-response';

Expand All @@ -33,9 +33,9 @@ export async function POST(request: NextRequest) {

// Validate request
const body = await request.json();
const errorMessage = unknownValidator(body, AnswerSelectionWithGameId);
const errorMessage = unknownValidator(body, AnswerSelectionWithGameIdSchema);
if (errorMessage) return badRequestResponse({errorMessage});
const {gameId, answerSelection} = unknownParser(body, AnswerSelectionWithGameId);
const {gameId, answerSelection} = unknownParser(body, AnswerSelectionWithGameIdSchema);

const gameRef = await gamesRef.doc(gameId);
const gameDoc = await gameRef.get();
Expand Down
6 changes: 3 additions & 3 deletions app-dev/party-game/app/components/create-game-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {useRouter} from 'next/navigation';
import {useEffect, useState} from 'react';
import BigColorBorderButton from './big-color-border-button';
import {unknownParser, unknownValidator} from '@/app/lib/zod-parser';
import {GameIdObject, GameSettings} from '@/app/types/zod-types';
import {GameIdObjectSchema, GameSettingsSchema} from '@/app/types';

export default function CreateGameForm() {
const authUser = useFirebaseAuthentication();
Expand All @@ -45,7 +45,7 @@ export default function CreateGameForm() {
},
});
const response = await res.json();
const parsedResponse = unknownParser(response, GameIdObject);
const parsedResponse = unknownParser(response, GameIdObjectSchema);
if (!parsedResponse.gameId) throw new Error('no gameId returned in the response');
router.push(`/game/${parsedResponse.gameId}`);
} catch (error) {
Expand All @@ -54,7 +54,7 @@ export default function CreateGameForm() {
};

useEffect(() => {
setErrorMessage(unknownValidator({timePerAnswer, timePerQuestion}, GameSettings));
setErrorMessage(unknownValidator({timePerAnswer, timePerQuestion}, GameSettingsSchema));
}, [timePerAnswer, timePerQuestion]);

return (
Expand Down
4 changes: 2 additions & 2 deletions app-dev/party-game/app/hooks/use-firebase-authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ export const emptyUser: User = {
delete: function(): Promise<void> {
throw new Error('Function not implemented.');
},
getIdToken: function(forceRefresh?: boolean | undefined): Promise<string> {
getIdToken: function(): Promise<string> {
throw new Error('Function not implemented.');
},
getIdTokenResult: function(forceRefresh?: boolean | undefined): Promise<IdTokenResult> {
getIdTokenResult: function(): Promise<IdTokenResult> {
throw new Error('Function not implemented.');
},
reload: function(): Promise<void> {
Expand Down
10 changes: 6 additions & 4 deletions app-dev/party-game/app/hooks/use-game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@

import {useEffect, useState} from 'react';
import {db} from '@/app/lib/firebase-client-initialization';
import {Game, emptyGame, gameStates} from '@/app/types';
import {Game, GameSchema, emptyGame, gameStates} from '@/app/types';
import {doc, onSnapshot} from 'firebase/firestore';
import {usePathname} from 'next/navigation';
import useFirebaseAuthentication from './use-firebase-authentication';
import {unknownParser} from '../lib/zod-parser';

const useGame = () => {
const pathname = usePathname();
Expand Down Expand Up @@ -56,10 +57,11 @@ const useGame = () => {
useEffect(() => {
const gameRef = doc(db, 'games', gameId);
const unsubscribe = onSnapshot(gameRef, (doc) => {
const game = doc.data() as Game;
if (game) {
try {
const game = unknownParser(doc.data(), GameSchema);
setGame(game);
} else {
} catch (error) {
console.log(error);
setErrorMessage(`Game ${gameId} was not found.`);
}
});
Expand Down
2 changes: 1 addition & 1 deletion app-dev/party-game/app/lib/server-side-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ export async function getAuthenticatedUser(request: NextRequest): Promise<Decode
const token = request.headers.get('Authorization') || '';
const decodedIdToken = await getAuth(app).verifyIdToken(token);
return decodedIdToken;
};
}
2 changes: 1 addition & 1 deletion app-dev/party-game/app/lib/zod-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const unknownValidator = (body: unknown, Schema: z.ZodType) => {
} catch (error) {
// return the first error
if (error instanceof z.ZodError) {
return error.issues[0].message;
return `${error.issues[0].message}`;
}
throw error;
}
Expand Down
115 changes: 53 additions & 62 deletions app-dev/party-game/app/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,75 +14,66 @@
* limitations under the License.
*/

export type Answer = {
isCorrect: boolean;
isSelected: boolean;
text: string;
}
import {z} from 'zod';

export type Question = {
answers: Array<Answer>;
prompt: string;
explanation: string;
playerGuesses: {
[key: string]: Boolean[];
};
}
const GameIdSchema = z.string();

export const emptyQuestion: Question = {
answers: [],
prompt: '',
explanation: '',
playerGuesses: {},
};
export const GameIdObjectSchema = z.object({gameId: GameIdSchema});

export const gameStates = {
NOT_STARTED: 'NOT_STARTED',
SHOWING_CORRECT_ANSWERS: 'SHOWING_CORRECT_ANSWERS',
AWAITING_PLAYER_ANSWERS: 'AWAITING_PLAYER_ANSWERS',
GAME_OVER: 'GAME_OVER',
} as const;
export const AnswerSelectionSchema = z.array(z.boolean());

export type GameState = (typeof gameStates)[keyof typeof gameStates];
export const AnswerSelectionWithGameIdSchema = z.object({
gameId: GameIdSchema,
answerSelection: AnswerSelectionSchema,
});

export type Leader = {
uid: string;
displayName: string;
}
const TimePerQuestionSchema = z.number({invalid_type_error: 'Time per question must be a number'}).int().max(600, 'Time per question must be 600 or less.').min(10, 'Time per question must be at least 10.');
const TimePerAnswerSchema = z.number({invalid_type_error: 'Time per answer must be a number'}).int().max(600, 'Time per answer must be 600 or less.').min(5, 'Time per answer must be at least 5.');

export const emptyLeader = {
uid: '',
displayName: '',
};
export const GameSettingsSchema = z.object({timePerQuestion: TimePerQuestionSchema, timePerAnswer: TimePerAnswerSchema});

export type Players = {
[key: string]: string;
}
const AnswerSchema = z.object({
isCorrect: z.boolean(),
isSelected: z.boolean().default(false),
text: z.string(),
});
export type Answer = z.infer<typeof AnswerSchema>;

export type Game = {
questions: Array<Question>;
leader: Leader,
players: Players;
state: GameState;
currentQuestionIndex: number;
startTime: any;
timePerQuestion: number;
timePerAnswer: number;
}
export const QuestionSchema = z.object({
answers: z.array(AnswerSchema).default([]),
prompt: z.string().default(''),
explanation: z.string().default(''),
playerGuesses: z.record(z.string(), z.array(z.boolean())).default({}),
});
export const emptyQuestion = QuestionSchema.parse({});
export type Question = z.infer<typeof QuestionSchema>;

export const emptyGame: Game = {
questions: [],
leader: emptyLeader,
players: {},
state: gameStates.NOT_STARTED,
currentQuestionIndex: -1,
startTime: '',
timePerQuestion: -1,
timePerAnswer: -1,
};
const gameStatesOptions = ['NOT_STARTED', 'SHOWING_CORRECT_ANSWERS', 'AWAITING_PLAYER_ANSWERS', 'GAME_OVER'] as const;
const GameStateEnum = z.enum(gameStatesOptions);
export const gameStates = GameStateEnum.Values;

export type RouteWithCurrentStatus = {
name: string;
href: string;
current: boolean;
}
export const LeaderSchema = z.object({
uid: z.string().default(''),
displayName: z.string().default(''),
});
const emptyLeader = LeaderSchema.parse({});

export const GameSchema = z.object({
questions: z.record(z.string(), QuestionSchema).default({}),
leader: LeaderSchema.default(emptyLeader),
players: z.record(z.string(), z.string()).default({}),
state: GameStateEnum.default(gameStates.NOT_STARTED),
currentQuestionIndex: z.number().int().nonnegative().default(0),
startTime: z.object({seconds: z.number()}).default({seconds: -1}),
timePerQuestion: TimePerQuestionSchema.default(60),
timePerAnswer: TimePerAnswerSchema.default(20),
});
export type Game = z.infer<typeof GameSchema>;
export const emptyGame = GameSchema.parse({});

const RouteWithCurrentStatusSchema = z.object({
name: z.string(),
href: z.string(),
current: z.string(),
});
export type RouteWithCurrentStatus = z.infer<typeof RouteWithCurrentStatusSchema>;
Loading

0 comments on commit 398a88b

Please sign in to comment.