json schema validation

This commit is contained in:
Aode (lion) 2021-12-30 16:43:52 -06:00
parent 3f254b7f66
commit a7184c3853
11 changed files with 330 additions and 124 deletions

View file

@ -4,3 +4,5 @@ node_modules
dist
# Ignore eslintrc
.eslintrc.js
# Ignore webpack
webpack.config.js

View file

@ -14,6 +14,7 @@
"react-dom": "^17.0.2",
"react-scripts": "5.0.0",
"typescript": "^4.5.4",
"util": "^0.12.4",
"web-vitals": "^2.1.2"
},
"scripts": {

View file

@ -1,5 +1,6 @@
import React from "react";
import Board, { BoardState, Coordinates, Selected } from "./Board";
import Board, { Coordinates, Selected } from "./Board";
import BoardState from "./boardState";
import { init } from "./client";
import "./App.css";

View file

@ -1,20 +1,11 @@
import React from "react";
type COLOR = "black" | "white";
type PIECE = "pawn" | "rook" | "knight" | "bishop" | "queen" | "king";
type FILE = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h";
type RANK = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
import BoardState, { COLOR, FILE, PIECE, RANK, PieceState } from "./boardState";
export type Coordinates = {
rank: RANK;
file: FILE;
};
export type PieceState = {
kind: PIECE;
color: COLOR;
};
export type Selected = {
piece: PieceState;
location: Coordinates;
@ -43,13 +34,6 @@ export interface CellProps {
onSelectDestination: OnSelectDestination;
}
export type FileState = {
[key in RANK]?: PieceState;
};
export type BoardState = {
[key in FILE]?: FileState;
};
const RANKS: RANK[] = [1, 2, 3, 4, 5, 6, 7, 8];
const FILES: FILE[] = ["a", "b", "c", "d", "e", "f", "g", "h"];
@ -114,10 +98,10 @@ const Cell = ({
const classNames =
selected !== null &&
selected.piece.kind === piece.kind &&
selected.piece.color === piece.color &&
selected.location.file === file &&
selected.location.rank === rank
selected.piece.kind === piece.kind &&
selected.piece.color === piece.color &&
selected.location.file === file &&
selected.location.rank === rank
? ["icon-cell", "selected"]
: ["icon-cell"];

19
src/boardState.ts Normal file
View file

@ -0,0 +1,19 @@
export type COLOR = "black" | "white";
export type PIECE = "pawn" | "rook" | "knight" | "bishop" | "queen" | "king";
export type FILE = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h";
export type RANK = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
export type PieceState = {
kind: PIECE;
color: COLOR;
};
export type FileState = {
[key in RANK]?: PieceState;
};
export type BoardState = {
[key in FILE]?: FileState;
};
export default BoardState;

View file

@ -1,4 +1,4 @@
import { validateBoardState } from "./validate";
import validate from "./boardState.validator";
test("validates valid states", () => {
const inputs = [
@ -43,7 +43,7 @@ test("validates valid states", () => {
];
inputs.map((input) => {
const result = validateBoardState(input);
const result = validate(input);
expect(result).not.toBeNull();
});
@ -92,8 +92,6 @@ test("does not validate invalid states", () => {
];
inputs.map((input) => {
const result = validateBoardState(input);
expect(result).toBeNull();
expect(() => validate(input)).toThrow();
});
});

232
src/boardState.validator.ts Normal file
View file

@ -0,0 +1,232 @@
/* tslint:disable */
// generated by typescript-json-validator
import { inspect } from 'util';
import Ajv, { ErrorObject } from 'ajv';
import BoardState from './boardState';
import schema from 'ajv/lib/refs/json-schema-draft-06.json';
export const ajv = new Ajv({ "allErrors": true, "coerceTypes": false, "format": "fast", "nullable": true, "unicode": true, "uniqueItems": true, "useDefaults": true });
ajv.addMetaSchema(schema);
export const BoardStateSchema = {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"defaultProperties": [
],
"definitions": {
"COLOR": {
"enum": [
"black",
"white"
],
"type": "string"
},
"FileState": {
"additionalProperties": false,
"defaultProperties": [
],
"properties": {
"1": {
"additionalProperties": false,
"defaultProperties": [
],
"properties": {
"color": {
"$ref": "#/definitions/COLOR"
},
"kind": {
"$ref": "#/definitions/PIECE"
}
},
"required": [
"color",
"kind"
],
"type": "object"
},
"2": {
"additionalProperties": false,
"defaultProperties": [
],
"properties": {
"color": {
"$ref": "#/definitions/COLOR"
},
"kind": {
"$ref": "#/definitions/PIECE"
}
},
"required": [
"color",
"kind"
],
"type": "object"
},
"3": {
"additionalProperties": false,
"defaultProperties": [
],
"properties": {
"color": {
"$ref": "#/definitions/COLOR"
},
"kind": {
"$ref": "#/definitions/PIECE"
}
},
"required": [
"color",
"kind"
],
"type": "object"
},
"4": {
"additionalProperties": false,
"defaultProperties": [
],
"properties": {
"color": {
"$ref": "#/definitions/COLOR"
},
"kind": {
"$ref": "#/definitions/PIECE"
}
},
"required": [
"color",
"kind"
],
"type": "object"
},
"5": {
"additionalProperties": false,
"defaultProperties": [
],
"properties": {
"color": {
"$ref": "#/definitions/COLOR"
},
"kind": {
"$ref": "#/definitions/PIECE"
}
},
"required": [
"color",
"kind"
],
"type": "object"
},
"6": {
"additionalProperties": false,
"defaultProperties": [
],
"properties": {
"color": {
"$ref": "#/definitions/COLOR"
},
"kind": {
"$ref": "#/definitions/PIECE"
}
},
"required": [
"color",
"kind"
],
"type": "object"
},
"7": {
"additionalProperties": false,
"defaultProperties": [
],
"properties": {
"color": {
"$ref": "#/definitions/COLOR"
},
"kind": {
"$ref": "#/definitions/PIECE"
}
},
"required": [
"color",
"kind"
],
"type": "object"
},
"8": {
"additionalProperties": false,
"defaultProperties": [
],
"properties": {
"color": {
"$ref": "#/definitions/COLOR"
},
"kind": {
"$ref": "#/definitions/PIECE"
}
},
"required": [
"color",
"kind"
],
"type": "object"
}
},
"type": "object"
},
"PIECE": {
"enum": [
"bishop",
"king",
"knight",
"pawn",
"queen",
"rook"
],
"type": "string"
}
},
"properties": {
"a": {
"$ref": "#/definitions/FileState"
},
"b": {
"$ref": "#/definitions/FileState"
},
"c": {
"$ref": "#/definitions/FileState"
},
"d": {
"$ref": "#/definitions/FileState"
},
"e": {
"$ref": "#/definitions/FileState"
},
"f": {
"$ref": "#/definitions/FileState"
},
"g": {
"$ref": "#/definitions/FileState"
},
"h": {
"$ref": "#/definitions/FileState"
}
},
"type": "object"
};
export type ValidateFunction<T> = ((data: unknown) => data is T) & Pick<Ajv.ValidateFunction, 'errors'>
export const isBoardState = ajv.compile(BoardStateSchema) as ValidateFunction<BoardState>;
export default function validate(value: unknown): BoardState {
if (isBoardState(value)) {
return value;
} else {
if (Array.isArray(isBoardState.errors)) {
throw new Error(
ajv.errorsText(isBoardState.errors.filter((e: ErrorObject) => e.keyword !== 'if'), { dataVar: 'BoardState' }) +
'\n\n' +
inspect(value),
);
}
throw new Error("Unknown error");
}
}

View file

@ -1,5 +1,6 @@
import { BoardState, Selected, Coordinates } from "./Board";
import { validateBoardState } from "./validate";
import { Selected, Coordinates } from "./Board";
import BoardState from "./boardState";
import validate from "./boardState.validator";
export type StartGame = () => Promise<BoardState | null>;
export type MakeMove = (
@ -40,7 +41,7 @@ const startGame = (baseUri: string): StartGame => {
const json: unknown = await response.json();
return validateBoardState(json);
return validate(json);
} catch (error) {
console.error("Failed to start game,", error);
return null;
@ -80,7 +81,7 @@ const makeMove = (baseUri: string): MakeMove => {
const json: unknown = await response.json();
return validateBoardState(json);
return validate(json);
} catch (error) {
console.error("Failed to make move,", error);
return null;

View file

@ -1,91 +0,0 @@
import { COLORS, PIECES, FILES, RANKS, BoardState } from "./Board";
const COLORS_AS_STRINGS: string[] = COLORS.map((color) => color as string);
const PIECES_AS_STRINGS: string[] = PIECES.map((piece) => piece as string);
const FILES_AS_STRINGS: string[] = FILES.map((file) => file as string);
const RANKS_AS_NUMBERS: number[] = RANKS.map((rank) => rank as number);
const validateBoardState = (input: unknown): BoardState | null => {
if (typeof input !== "object" || Array.isArray(input) || input === null) {
console.error("Input is not keyed object");
return null;
}
const hasInvalidShape = Object.entries(input).some(([file, ranks]) => {
if (typeof file !== "string") {
console.error("file is not string");
return true;
}
if (!FILES_AS_STRINGS.includes(file)) {
console.error("file is not included in valid files");
return true;
}
if (typeof ranks !== "object" || Array.isArray(ranks) || ranks === null) {
console.error("ranks is not a keyed object");
return true;
}
return Object.entries(ranks as { [s: string]: unknown }).some(
([rank, pieces]) => {
if (typeof rank !== "string") {
console.error("rank is not a string");
return true;
}
if (!RANKS_AS_NUMBERS.includes(Number(rank))) {
console.error("rank is not included in valid ranks");
return true;
}
if (
typeof pieces !== "object" ||
Array.isArray(pieces) ||
pieces === null
) {
console.error("piece state is not a keyed object");
return true;
}
return Object.entries(pieces).some(([key, value]) => {
if (typeof key !== "string") {
console.error("piece key is not a string");
return true;
}
if (!["kind", "color"].includes(key)) {
console.error("piece key is not included in valid keys");
return true;
}
if (
key === "kind" &&
(typeof value !== "string" || !PIECES_AS_STRINGS.includes(value))
) {
console.error("kind's value is not a string, or not a valid piece");
return true;
} else if (
key === "color" &&
(typeof value !== "string" || !COLORS_AS_STRINGS.includes(value))
) {
console.error(
"colors' value is not a string, or not a valid color"
);
return true;
}
return false;
});
}
);
});
if (hasInvalidShape) {
return null;
}
return input as BoardState;
};
export { validateBoardState };

7
webpack.config.js Normal file
View file

@ -0,0 +1,7 @@
module.exports = {
resolve: {
fallback: {
util: require.resolve("util/"),
},
},
};

View file

@ -2355,6 +2355,11 @@ autoprefixer@^10.4.0:
picocolors "^1.0.0"
postcss-value-parser "^4.2.0"
available-typed-arrays@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==
axe-core@^4.3.5:
version "4.3.5"
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.3.5.tgz#78d6911ba317a8262bfee292aeafcc1e04b49cc5"
@ -3582,7 +3587,7 @@ error-stack-parser@^2.0.6:
dependencies:
stackframe "^1.1.1"
es-abstract@^1.17.2, es-abstract@^1.19.0, es-abstract@^1.19.1:
es-abstract@^1.17.2, es-abstract@^1.18.5, es-abstract@^1.19.0, es-abstract@^1.19.1:
version "1.19.1"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3"
integrity sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==
@ -4155,6 +4160,11 @@ follow-redirects@^1.0.0:
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.6.tgz#8cfb281bbc035b3c067d6cd975b0f6ade6e855cd"
integrity sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A==
foreach@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"
integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k=
fork-ts-checker-webpack-plugin@^6.5.0:
version "6.5.0"
resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.0.tgz#0282b335fa495a97e167f69018f566ea7d2a2b5e"
@ -4762,6 +4772,13 @@ is-generator-fn@^2.0.0:
resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118"
integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==
is-generator-function@^1.0.7:
version "1.0.10"
resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72"
integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==
dependencies:
has-tostringtag "^1.0.0"
is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
version "4.0.3"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
@ -4858,6 +4875,17 @@ is-symbol@^1.0.2, is-symbol@^1.0.3:
dependencies:
has-symbols "^1.0.2"
is-typed-array@^1.1.3, is-typed-array@^1.1.7:
version "1.1.8"
resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.8.tgz#cbaa6585dc7db43318bc5b89523ea384a6f65e79"
integrity sha512-HqH41TNZq2fgtGT8WHVFVJhBVGuY3AnP3Q36K8JKXUxSxRgk/d+7NjmwG2vo2mYmXK8UYZKu0qH8bVP5gEisjA==
dependencies:
available-typed-arrays "^1.0.5"
call-bind "^1.0.2"
es-abstract "^1.18.5"
foreach "^2.0.5"
has-tostringtag "^1.0.0"
is-typedarray@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
@ -7302,7 +7330,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0:
safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
@ -8201,6 +8229,18 @@ util.promisify@~1.0.0:
has-symbols "^1.0.1"
object.getownpropertydescriptors "^2.1.0"
util@^0.12.4:
version "0.12.4"
resolved "https://registry.yarnpkg.com/util/-/util-0.12.4.tgz#66121a31420df8f01ca0c464be15dfa1d1850253"
integrity sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw==
dependencies:
inherits "^2.0.3"
is-arguments "^1.0.4"
is-generator-function "^1.0.7"
is-typed-array "^1.1.3"
safe-buffer "^5.1.2"
which-typed-array "^1.1.2"
utila@~0.4:
version "0.4.0"
resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c"
@ -8456,6 +8496,18 @@ which-boxed-primitive@^1.0.2:
is-string "^1.0.5"
is-symbol "^1.0.3"
which-typed-array@^1.1.2:
version "1.1.7"
resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.7.tgz#2761799b9a22d4b8660b3c1b40abaa7739691793"
integrity sha512-vjxaB4nfDqwKI0ws7wZpxIlde1XrLX5uB0ZjpfshgmapJMD7jJWhZI+yToJTqaFByF0eNBcYxbjmCzoRP7CfEw==
dependencies:
available-typed-arrays "^1.0.5"
call-bind "^1.0.2"
es-abstract "^1.18.5"
foreach "^2.0.5"
has-tostringtag "^1.0.0"
is-typed-array "^1.1.7"
which@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"