monkeytype/frontend/scripts/json-validation.js

603 lines
17 KiB
JavaScript
Raw Normal View History

2022-02-19 01:08:22 +08:00
const fs = require("fs");
const V = require("jsonschema").Validator;
const JSONValidator = new V();
function escapeRegExp(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function findDuplicates(words) {
const wordFrequencies = {};
const duplicates = [];
words.forEach((word) => {
wordFrequencies[word] =
word in wordFrequencies ? wordFrequencies[word] + 1 : 1;
if (wordFrequencies[word] === 2) {
duplicates.push(word);
}
});
return duplicates;
}
2022-02-19 02:25:33 +08:00
function validateOthers() {
2022-02-19 01:08:22 +08:00
return new Promise((resolve, reject) => {
//fonts
const fontsData = JSON.parse(
fs.readFileSync("./static/fonts/_list.json", {
encoding: "utf8",
flag: "r",
})
);
const fontsSchema = {
type: "array",
items: {
type: "object",
properties: {
name: {
type: "string",
},
},
required: ["name"],
},
};
const fontsValidator = JSONValidator.validate(fontsData, fontsSchema);
if (fontsValidator.valid) {
console.log("Fonts JSON schema is \u001b[32mvalid\u001b[0m");
} else {
console.log("Fonts JSON schema is \u001b[31minvalid\u001b[0m");
return reject(new Error(fontsValidator.errors));
}
//funbox
const funboxData = JSON.parse(
fs.readFileSync("./static/funbox/_list.json", {
encoding: "utf8",
flag: "r",
})
);
const funboxSchema = {
type: "array",
items: {
type: "object",
properties: {
name: { type: "string" },
info: { type: "string" },
Multiple funboxes (#3578) egorguslyan * input-controller * result * Finishing logic * Numbers + layoutfluid * One interface * Filter results * tts error on undefined Extencions like NoScript can partly block scripts on the page. If speech synthesis is not loaded, notification shows up without freezing the page * Improved randomcase * Prevent dublicates in command line * Change filter logic * Prettier * Convert numbers * num * Quote and zen modes * withWords * Misc * Expand funboxes list for pb saving * Move list to backend * Move to constants * Async withWords, checkFunbox tweak * Prettier * Forbid nonexistent funboxes * Disable speech if language is ignored TtS's init() uses setLanguage() * canGetPb * Less circular imports * Ligatures typo * Simon says blocks word highlight * blockWordHighlight backend * Changed imports * usesLayout * JSON schema * Display notification instead of reseting * canGetPB * One getWordHtml * Dividing properties * No sync * blockedModes * forcedConfig * Infinitness parameter, list sync * applyConfig, memory Remove extra applyConfig somewhere; Memory in quotes and custom modes * I lost this code after merging * Remove arrowKeys * isFunboxCompatible * Fix logic * sync canGetPb * remove FunboxObjectType * baloons * moved cangetpb function to pb utils * updated the pb check to be easier to understand * Refactor isFunboxCompatible * Check modes conflicts * Strict highlightMode type * Only one allowed or blocked highlight mode * More checks * Undefined only, not false * Prettier * Highlight modes * added intersect helper function * reworked forced config - storing allowed modes as an array, not string - first value will be used if config is outside of the allowed values - instead of checking if highlight mode is allowed, checking if the whole config is available - removed the "Finite" forced config and replaced it with "noInfiniteDuration" property - config event listener now checks every config change, not just highlight mode. this will ensure any future forced configs will work straight out of the box * ManualRestart in commandline * fixed funbox commands not correctly showing which funbox is active * Upd list * Reduce list * split funbox into smaller files moved funbox files into its own folder * missing none command * added function to convert camel case to space separated words * changed config validation to be blocking the change rather than reacting to the change * reduced code duplication * allowing sub color flash * moved keymap key higlighting and flashing into an observable event * moved tts into a observable event * passing funbox into config validation funcitons * replaced getActive with get * only keeping functions structure in the list, moved the actual function bodies to funbox.ts done to remove a circular dependency still need to finish the rest of the funboxes * removed empty function definitions (typing issues) * removed unnecessary type * unnecessary check * moved mode checking to config validation * longer notification * checking funboxes before changing mode * moved more functions * fixed incorrect type * checking funboxes when setting punctuation and numbers * Rest of funboxes * fixed funbox commands showing tags text and icon * checking if funbox can be set with the current config * better error message * validating with setting time and words importing from a new file * added a function to capitalise the first letter of a string * using function from a new file new parameters * moved test length check to a function in a different file * moved some funbox validation into its own file * only showing notifications if the setWordCount returned true * moved funbox validation to its own file * setting manual restart when trying to set funbox to nonoe * moving this validation to before activating the funbox * returning forcedConfigs along side if current value is allowed moved infinite check to checkFunboxForcedConfigs * removed function, replaced by funox validation * removing duplicates * throwing if no intersection * wrong type * always allowing setting funbox sometimes it might be possible to update the config * checking forced configs first, and updating config if possible only setting funbox to none when couldnt update config * basic difficulty levels * xp funbox bonus * removed console logs * renamed import, renamed type * lowercase b for consistency across the codebase * renamed variable for readability * renamed for clarity * converted metadata to object * changed from beforesubgroup on the command to before list on the subgroup * using code suggested by bruce * renamed type * removed console log * merch banner fix * important animation * updating the icon of "none" funbox command * removed unnecessary import Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Miodec <jack@monkeytype.com>
2022-11-30 23:57:48 +08:00
canGetPb: { type: "boolean" },
alias: { type: "string" },
2022-02-19 01:08:22 +08:00
},
Multiple funboxes (#3578) egorguslyan * input-controller * result * Finishing logic * Numbers + layoutfluid * One interface * Filter results * tts error on undefined Extencions like NoScript can partly block scripts on the page. If speech synthesis is not loaded, notification shows up without freezing the page * Improved randomcase * Prevent dublicates in command line * Change filter logic * Prettier * Convert numbers * num * Quote and zen modes * withWords * Misc * Expand funboxes list for pb saving * Move list to backend * Move to constants * Async withWords, checkFunbox tweak * Prettier * Forbid nonexistent funboxes * Disable speech if language is ignored TtS's init() uses setLanguage() * canGetPb * Less circular imports * Ligatures typo * Simon says blocks word highlight * blockWordHighlight backend * Changed imports * usesLayout * JSON schema * Display notification instead of reseting * canGetPB * One getWordHtml * Dividing properties * No sync * blockedModes * forcedConfig * Infinitness parameter, list sync * applyConfig, memory Remove extra applyConfig somewhere; Memory in quotes and custom modes * I lost this code after merging * Remove arrowKeys * isFunboxCompatible * Fix logic * sync canGetPb * remove FunboxObjectType * baloons * moved cangetpb function to pb utils * updated the pb check to be easier to understand * Refactor isFunboxCompatible * Check modes conflicts * Strict highlightMode type * Only one allowed or blocked highlight mode * More checks * Undefined only, not false * Prettier * Highlight modes * added intersect helper function * reworked forced config - storing allowed modes as an array, not string - first value will be used if config is outside of the allowed values - instead of checking if highlight mode is allowed, checking if the whole config is available - removed the "Finite" forced config and replaced it with "noInfiniteDuration" property - config event listener now checks every config change, not just highlight mode. this will ensure any future forced configs will work straight out of the box * ManualRestart in commandline * fixed funbox commands not correctly showing which funbox is active * Upd list * Reduce list * split funbox into smaller files moved funbox files into its own folder * missing none command * added function to convert camel case to space separated words * changed config validation to be blocking the change rather than reacting to the change * reduced code duplication * allowing sub color flash * moved keymap key higlighting and flashing into an observable event * moved tts into a observable event * passing funbox into config validation funcitons * replaced getActive with get * only keeping functions structure in the list, moved the actual function bodies to funbox.ts done to remove a circular dependency still need to finish the rest of the funboxes * removed empty function definitions (typing issues) * removed unnecessary type * unnecessary check * moved mode checking to config validation * longer notification * checking funboxes before changing mode * moved more functions * fixed incorrect type * checking funboxes when setting punctuation and numbers * Rest of funboxes * fixed funbox commands showing tags text and icon * checking if funbox can be set with the current config * better error message * validating with setting time and words importing from a new file * added a function to capitalise the first letter of a string * using function from a new file new parameters * moved test length check to a function in a different file * moved some funbox validation into its own file * only showing notifications if the setWordCount returned true * moved funbox validation to its own file * setting manual restart when trying to set funbox to nonoe * moving this validation to before activating the funbox * returning forcedConfigs along side if current value is allowed moved infinite check to checkFunboxForcedConfigs * removed function, replaced by funox validation * removing duplicates * throwing if no intersection * wrong type * always allowing setting funbox sometimes it might be possible to update the config * checking forced configs first, and updating config if possible only setting funbox to none when couldnt update config * basic difficulty levels * xp funbox bonus * removed console logs * renamed import, renamed type * lowercase b for consistency across the codebase * renamed variable for readability * renamed for clarity * converted metadata to object * changed from beforesubgroup on the command to before list on the subgroup * using code suggested by bruce * renamed type * removed console log * merch banner fix * important animation * updating the icon of "none" funbox command * removed unnecessary import Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Miodec <jack@monkeytype.com>
2022-11-30 23:57:48 +08:00
required: ["name", "info", "canGetPb"],
2022-02-19 01:08:22 +08:00
},
};
const funboxValidator = JSONValidator.validate(funboxData, funboxSchema);
if (funboxValidator.valid) {
console.log("Funbox JSON schema is \u001b[32mvalid\u001b[0m");
} else {
console.log("Funbox JSON schema is \u001b[31minvalid\u001b[0m");
return reject(new Error(funboxValidator.errors));
}
//themes
const themesData = JSON.parse(
fs.readFileSync("./static/themes/_list.json", {
encoding: "utf8",
flag: "r",
})
);
const themesSchema = {
type: "array",
items: {
type: "object",
properties: {
name: { type: "string" },
bgColor: { type: "string" },
mainColor: { type: "string" },
},
required: ["name", "bgColor", "mainColor"],
},
};
const themesValidator = JSONValidator.validate(themesData, themesSchema);
if (themesValidator.valid) {
console.log("Themes JSON schema is \u001b[32mvalid\u001b[0m");
} else {
console.log("Themes JSON schema is \u001b[31minvalid\u001b[0m");
return reject(new Error(themesValidator.errors));
}
//challenges
const challengesSchema = {
type: "array",
items: {
type: "object",
properties: {
name: { type: "string" },
display: { type: "string" },
autoRole: { type: "boolean" },
type: { type: "string" },
message: { type: "string" },
parameters: {
type: "array",
},
requirements: {
type: "object",
properties: {
wpm: {
type: "object",
properties: {
min: { type: "number" },
max: { type: "number" },
exact: { type: "number" },
},
},
time: {
type: "object",
properties: {
min: { type: "number" },
max: { type: "number" },
exact: { type: "number" },
},
},
acc: {
type: "object",
properties: {
min: { type: "number" },
max: { type: "number" },
exact: { type: "number" },
},
},
raw: {
type: "object",
properties: {
min: { type: "number" },
max: { type: "number" },
exact: { type: "number" },
},
},
con: {
type: "object",
properties: {
min: { type: "number" },
max: { type: "number" },
exact: { type: "number" },
},
},
config: {
type: "object",
},
2022-02-21 20:26:55 +08:00
funbox: {
type: "object",
properties: {
exact: { type: "string" },
},
},
2022-02-19 01:08:22 +08:00
},
},
},
required: ["name", "display", "type", "parameters"],
},
};
const challengesData = JSON.parse(
fs.readFileSync("./static/challenges/_list.json", {
encoding: "utf8",
flag: "r",
})
);
const challengesValidator = JSONValidator.validate(
challengesData,
challengesSchema
);
if (challengesValidator.valid) {
console.log("Challenges list JSON schema is \u001b[32mvalid\u001b[0m");
} else {
console.log("Challenges list JSON schema is \u001b[31minvalid\u001b[0m");
return reject(new Error(challengesValidator.errors));
}
//layouts
const layoutsSchema = {
ansi: {
type: "object",
properties: {
keymapShowTopRow: { type: "boolean" },
type: { type: "string", pattern: "^ansi$" },
keys: {
type: "object",
properties: {
row1: {
type: "array",
items: { type: "string", minLength: 1, maxLength: 2 },
minItems: 13,
maxItems: 13,
},
row2: {
type: "array",
items: { type: "string", minLength: 1, maxLength: 2 },
minItems: 13,
maxItems: 13,
},
row3: {
type: "array",
items: { type: "string", minLength: 1, maxLength: 2 },
minItems: 11,
maxItems: 11,
},
row4: {
type: "array",
items: { type: "string", minLength: 1, maxLength: 2 },
minItems: 10,
maxItems: 10,
},
row5: {
type: "array",
items: { type: "string", minLength: 1, maxLength: 1 },
minItems: 1,
maxItems: 1,
},
},
required: ["row1", "row2", "row3", "row4", "row5"],
},
},
required: ["keymapShowTopRow", "type", "keys"],
},
iso: {
type: "object",
properties: {
keymapShowTopRow: { type: "boolean" },
type: { type: "string", pattern: "^iso$" },
keys: {
type: "object",
properties: {
row1: {
type: "array",
items: { type: "string", minLength: 1, maxLength: 2 },
minItems: 13,
maxItems: 13,
},
row2: {
type: "array",
items: { type: "string", minLength: 1, maxLength: 2 },
minItems: 12,
maxItems: 12,
},
row3: {
type: "array",
items: { type: "string", minLength: 1, maxLength: 2 },
minItems: 12,
maxItems: 12,
},
row4: {
type: "array",
items: { type: "string", minLength: 1, maxLength: 2 },
minItems: 11,
maxItems: 11,
},
row5: {
type: "array",
items: { type: "string", minLength: 1, maxLength: 1 },
minItems: 1,
maxItems: 1,
},
},
required: ["row1", "row2", "row3", "row4", "row5"],
},
},
required: ["keymapShowTopRow", "type", "keys"],
},
};
const layoutsData = JSON.parse(
fs.readFileSync("./static/layouts/_list.json", {
encoding: "utf8",
flag: "r",
})
);
let layoutsAllGood = true;
let layoutsErrors;
Object.keys(layoutsData).forEach((layoutName) => {
const layoutData = layoutsData[layoutName];
2022-08-22 20:22:16 +08:00
if (!layoutsSchema[layoutData.type]) {
const msg = `Layout ${layoutName} has an invalid type: ${layoutData.type}`;
console.log(msg);
2022-02-19 01:08:22 +08:00
layoutsAllGood = false;
2022-08-22 20:22:16 +08:00
layoutsErrors = [msg];
} else {
const layoutsValidator = JSONValidator.validate(
layoutData,
layoutsSchema[layoutData.type]
);
if (!layoutsValidator.valid) {
console.log(
`Layout ${layoutName} JSON schema is \u001b[31minvalid\u001b[0m`
);
layoutsAllGood = false;
layoutsErrors = layoutsValidator.errors;
}
2022-02-19 01:08:22 +08:00
}
});
if (layoutsAllGood) {
console.log(`Layout JSON schemas are \u001b[32mvalid\u001b[0m`);
} else {
console.log(`Layout JSON schemas are \u001b[31minvalid\u001b[0m`);
return reject(new Error(layoutsErrors));
}
resolve();
});
}
2022-02-19 02:25:33 +08:00
function validateQuotes() {
return new Promise((resolve, reject) => {
//quotes
const quoteSchema = {
type: "object",
properties: {
language: { type: "string" },
groups: {
type: "array",
items: {
type: "array",
items: {
type: "number",
},
minItems: 2,
maxItems: 2,
},
},
quotes: {
type: "array",
items: {
type: "object",
properties: {
text: { type: "string" },
source: { type: "string" },
length: { type: "number" },
id: { type: "number" },
},
required: ["text", "source", "length", "id"],
},
},
},
required: ["language", "groups", "quotes"],
};
const quoteIdsSchema = {
type: "array",
items: {
type: "number",
},
uniqueItems: true,
};
let quoteFilesAllGood = true;
let quoteFilesErrors;
let quoteIdsAllGood = true;
let quoteIdsErrors;
2022-02-20 21:45:00 +08:00
let quoteLengthsAllGood = true;
let quoteLengthErrors = [];
2022-02-19 02:25:33 +08:00
const quotesFiles = fs.readdirSync("./static/quotes/");
quotesFiles.forEach((quotefilename) => {
quotefilename = quotefilename.split(".")[0];
const quoteData = JSON.parse(
fs.readFileSync(`./static/quotes/${quotefilename}.json`, {
encoding: "utf8",
flag: "r",
})
);
quoteSchema.properties.language.pattern =
"^" + escapeRegExp(quotefilename) + "$";
const quoteValidator = JSONValidator.validate(quoteData, quoteSchema);
if (!quoteValidator.valid) {
console.log(
`Quote ${quotefilename} JSON schema is \u001b[31minvalid\u001b[0m`
);
quoteFilesAllGood = false;
quoteFilesErrors = quoteValidator.errors;
}
const quoteIds = quoteData.quotes.map((quote) => quote.id);
const quoteIdsValidator = JSONValidator.validate(
quoteIds,
quoteIdsSchema
);
if (!quoteIdsValidator.valid) {
console.log(
`Quote ${quotefilename} IDs are \u001b[31mnot unique\u001b[0m`
);
quoteIdsAllGood = false;
quoteIdsErrors = quoteIdsValidator.errors;
}
2022-02-20 21:45:00 +08:00
const incorrectQuoteLength = quoteData.quotes
.filter((quote) => quote.text.length !== quote.length)
.map((quote) => quote.id);
if (incorrectQuoteLength.length !== 0) {
console.log(
`Quote ${quotefilename} ID(s) ${incorrectQuoteLength.join(
","
)} length fields are \u001b[31mincorrect\u001b[0m`
);
quoteFilesAllGood = false;
incorrectQuoteLength.map((id) => {
quoteLengthErrors.push(
`${quotefilename} ${id} length field is incorrect`
);
});
}
2022-02-19 02:25:33 +08:00
});
if (quoteFilesAllGood) {
console.log(`Quote file JSON schemas are \u001b[32mvalid\u001b[0m`);
} else {
console.log(`Quote file JSON schemas are \u001b[31minvalid\u001b[0m`);
return reject(new Error(quoteFilesErrors));
}
if (quoteIdsAllGood) {
console.log(`Quote IDs are \u001b[32munique\u001b[0m`);
} else {
console.log(`Quote IDs are \u001b[31mnot unique\u001b[0m`);
return reject(new Error(quoteIdsErrors));
}
2022-02-20 21:45:00 +08:00
if (quoteLengthsAllGood) {
console.log(`Quote length fields are \u001b[32mcorrect\u001b[0m`);
} else {
console.log(`Quote length fields are \u001b[31mincorrect\u001b[0m`);
return reject(new Error(quoteLengthErrors));
}
2022-02-19 02:25:33 +08:00
resolve();
});
}
function validateLanguages() {
return new Promise((resolve, reject) => {
//languages
const languagesData = JSON.parse(
fs.readFileSync("./static/languages/_list.json", {
encoding: "utf8",
flag: "r",
})
);
const languagesSchema = {
type: "array",
items: {
type: "string",
},
};
const languagesValidator = JSONValidator.validate(
languagesData,
languagesSchema
);
if (languagesValidator.valid) {
console.log("Languages list JSON schema is \u001b[32mvalid\u001b[0m");
} else {
console.log("Languages list JSON schema is \u001b[31minvalid\u001b[0m");
return reject(new Error(languagesValidator.errors));
}
//languages group
const languagesGroupData = JSON.parse(
fs.readFileSync("./static/languages/_groups.json", {
encoding: "utf8",
flag: "r",
})
);
const languagesGroupSchema = {
type: "array",
items: {
type: "object",
properties: {
name: { type: "string" },
languages: {
type: "array",
items: {
type: "string",
},
},
},
required: ["name", "languages"],
},
};
const languagesGroupValidator = JSONValidator.validate(
languagesGroupData,
languagesGroupSchema
);
if (languagesGroupValidator.valid) {
console.log("Languages groups JSON schema is \u001b[32mvalid\u001b[0m");
} else {
console.log("Languages groups JSON schema is \u001b[31minvalid\u001b[0m");
return reject(new Error(languagesGroupValidator.errors));
}
//language files
const languageFileSchema = {
type: "object",
properties: {
name: { type: "string" },
rightToLeft: { type: "boolean" },
2022-02-19 02:25:33 +08:00
noLazyMode: { type: "boolean" },
bcp47: { type: "string" },
words: {
type: "array",
items: { type: "string", minLength: 1 },
},
accents: {
type: "array",
items: {
type: "array",
items: { type: "string", minLength: 1 },
minItems: 2,
maxItems: 2,
},
},
},
required: ["name", "words"],
2022-02-19 02:25:33 +08:00
};
let languageFilesAllGood = true;
let languageWordListsAllGood = true;
2022-02-19 02:25:33 +08:00
let languageFilesErrors;
2022-05-22 06:50:40 +08:00
const duplicatePercentageThreshold = 0.0001;
2022-05-21 06:36:43 +08:00
let langsWithDuplicates = 0;
2022-02-19 02:25:33 +08:00
languagesData.forEach((language) => {
const languageFileData = JSON.parse(
fs.readFileSync(`./static/languages/${language}.json`, {
encoding: "utf8",
flag: "r",
})
);
languageFileSchema.properties.name.pattern =
"^" + escapeRegExp(language) + "$";
const languageFileValidator = JSONValidator.validate(
languageFileData,
languageFileSchema
);
if (!languageFileValidator.valid) {
languageFilesAllGood = false;
languageFilesErrors = languageFileValidator.errors;
return;
}
const duplicates = findDuplicates(languageFileData.words);
const duplicatePercentage =
(duplicates.length / languageFileData.words.length) * 100;
if (duplicatePercentage >= duplicatePercentageThreshold) {
2022-05-21 06:36:43 +08:00
langsWithDuplicates++;
languageWordListsAllGood = false;
languageFilesErrors = `Language '${languageFileData.name}' contains ${
duplicates.length
} (${Math.round(duplicatePercentage)}%) duplicates:`;
console.log(languageFilesErrors);
console.log(duplicates);
2022-02-19 02:25:33 +08:00
}
});
if (languageFilesAllGood) {
console.log(
`Language word list JSON schemas are \u001b[32mvalid\u001b[0m`
);
} else {
console.log(
`Language word list JSON schemas are \u001b[31minvalid\u001b[0m`
);
return reject(new Error(languageFilesErrors));
}
if (languageWordListsAllGood) {
console.log(
`Language word lists duplicate check is \u001b[32mvalid\u001b[0m`
);
} else {
console.log(
2022-05-21 06:36:43 +08:00
`Language word lists duplicate check is \u001b[31minvalid\u001b[0m (${langsWithDuplicates} languages contain duplicates)`
);
return reject(new Error(languageFilesErrors));
}
2022-02-19 02:25:33 +08:00
resolve();
});
}
function validateAll() {
return Promise.all([validateOthers(), validateLanguages(), validateQuotes()]);
}
2022-02-19 01:08:22 +08:00
module.exports = {
2022-02-19 02:25:33 +08:00
validateAll,
validateOthers,
validateLanguages,
validateQuotes,
2022-02-19 01:08:22 +08:00
};