From bc9d8484c5294ce2e4d8d744a7afc8a89e9e4c63 Mon Sep 17 00:00:00 2001 From: Edwin Kofler Date: Wed, 7 Aug 2024 04:34:16 -0700 Subject: [PATCH] Improve schema validation accuracy, error messages; cleanup --- cli.js | 1021 +++++++++++++------------- src/api/json/catalog.json | 2 +- src/schema-validation.jsonc | 9 - src/schema-validation.schema.json | 4 +- src/schemas/json/schema-catalog.json | 7 +- 5 files changed, 514 insertions(+), 529 deletions(-) diff --git a/cli.js b/cli.js index 498f0b781aa..5e78552c67b 100644 --- a/cli.js +++ b/cli.js @@ -3,6 +3,8 @@ import path from 'node:path' import fs from 'node:fs' import readline from 'node:readline' +import util from 'node:util' + import addFormats from 'ajv-formats' import ajvFormatsDraft2019 from 'ajv-formats-draft2019' import AjvDraft04 from 'ajv-draft-04' @@ -23,22 +25,27 @@ import minimist from 'minimist' const AjvDraft06SchemaJson = readJsonFile( 'node_modules/ajv/dist/refs/json-schema-draft-06.json', ) -const temporaryCoverageDir = './temp' -const schemaDir = './src/schemas/json' -const testPositiveDir = './src/test' -const testNegativeDir = './src/negative_test' -const urlSchemaStore = 'https://json.schemastore.org/' -const catalog = readJsonFile('./src/api/json/catalog.json') -const schemaValidation = /** @type {SchemaValidationJson} */ ( - jsoncParser.parse( - await fs.promises.readFile('./src/schema-validation.jsonc', 'utf-8'), - ) + +const CatalogFile = './src/api/json/catalog.json' +const Catalog = /** @type {CatalogJson} */ ( + jsoncParser.parse(await fs.promises.readFile(CatalogFile, 'utf-8')) +) + +const SchemaValidationFile = './src/schema-validation.jsonc' +const SchemaValidation = /** @type {SchemaValidationJson} */ ( + jsoncParser.parse(await fs.promises.readFile(SchemaValidationFile, 'utf-8')) ) -const [schemasToBeTested, foldersPositiveTest, foldersNegativeTest] = + +const TempCoverageDir = './temp' +const SchemaDir = './src/schemas/json' +const TestPositiveDir = './src/test' +const TestNegativeDir = './src/negative_test' +const UrlSchemaStore = 'https://json.schemastore.org/' +const [SchemasToBeTested, FoldersPositiveTest, FoldersNegativeTest] = await Promise.all([ - fs.promises.readdir(schemaDir), - fs.promises.readdir(testPositiveDir), - fs.promises.readdir(testNegativeDir), + fs.promises.readdir(SchemaDir), + fs.promises.readdir(TestPositiveDir), + fs.promises.readdir(TestNegativeDir), ]) // prettier-ignore @@ -64,57 +71,18 @@ const log = { console.error(error) } }, - writeln(/** @type {string | undefined} */ msg = '') { - console.log(msg) - }, -} - -function readJsonFile(/** @type {string} */ filename) { - return JSON.parse(fs.readFileSync(filename, 'utf-8')) } const argv = minimist(process.argv.slice(2), { boolean: ['help', 'lint'], }) -function skipThisFileName(/** @type {string} */ name) { - // This macOS file must always be ignored. - return name === '.DS_Store' -} - -function getUrlFromCatalog(catalogUrl) { - for (const schema of catalog.schemas) { - catalogUrl(schema.url) - const versions = schema.versions - if (versions) { - Object.values(versions).forEach((url) => catalogUrl(url)) - } - } -} - -/** - * @summary Calling this will terminate the process and show the text - * of each error message, in addition to npm's error message. - * @param {string[]} errorText - * @returns {never} - */ -function throwWithErrorText(errorText) { - log.writeln() - log.writeln() - log.writeln('################ Error message') - for (const text of errorText) { - log.error(text) - } - log.writeln('##############################') - throw new Error('See error message above this line.') -} - /** * @param {CbParamFn} schemaOnlyScan */ async function remoteSchemaFile(schemaOnlyScan, showLog = true) { - for (const { url } of catalog.schemas) { - if (url.startsWith(urlSchemaStore)) { + for (const { url } of Catalog.schemas) { + if (url.startsWith(UrlSchemaStore)) { // Skip local schemas continue } @@ -143,9 +111,9 @@ async function remoteSchemaFile(schemaOnlyScan, showLog = true) { } } catch (error) { if (showLog) { - log.writeln('') + console.log('') log.error(`Failed to fetch url: ${url}`, error) - log.writeln('') + console.log('') } } } @@ -179,6 +147,19 @@ async function remoteSchemaFile(schemaOnlyScan, showLog = true) { * @property {string[]} externalSchema */ +/** + * @typedef {Object} CatalogJsonEntry + * @property {string} name + * @property {string} description + * @property {string[]} fileMatch + * @property {string} url + */ + +/** + * @typedef {Object} CatalogJson + * @property {CatalogJsonEntry[]} schemas + */ + /** * @typedef {Object} SchemaValidationJson * @property {string[]} ajvNotStrictMode @@ -199,6 +180,51 @@ async function remoteSchemaFile(schemaOnlyScan, showLog = true) { * @property {boolean} schemaScan */ +function readJsonFile(/** @type {string} */ filename) { + return JSON.parse(fs.readFileSync(filename, 'utf-8')) +} + +function skipThisFileName(/** @type {string} */ name) { + // This macOS file must always be ignored. + return name === '.DS_Store' +} + +function getUrlFromCatalog(catalogUrl) { + for (const catalogEntry of Catalog.schemas) { + catalogUrl(catalogEntry.url) + const versions = catalogEntry.versions + if (versions) { + Object.values(versions).forEach((url) => catalogUrl(url)) + } + } +} + +/** + * @summary Calling this will terminate the process and show the text + * of each error message, in addition to npm's error message. + * @param {string[]} errorMessages + * @param {string=} errorString + * @returns {never} + */ +function printErrorMessagesAndExit(errorMessages, errorString) { + if (errorMessages.length > 0) { + console.warn('---') + log.error('Error Message:') + for (const text of errorMessages) { + log.error(text) + } + } + + if (errorString) { + process.stderr.write(errorString) + process.stderr.write('\n') + } + + console.warn('---') + console.trace('Error Message') + process.exit(1) +} + /** * @callback CbParamFn * @param {Schema} schema @@ -250,9 +276,9 @@ async function localSchemaFileAndTestFile( const schemaNameOption = argv.SchemaName if (processOnlyThisOneSchemaFile === undefined && schemaNameOption) { processOnlyThisOneSchemaFile = schemaNameOption - const file = path.join(schemaDir, processOnlyThisOneSchemaFile) + const file = path.join(SchemaDir, processOnlyThisOneSchemaFile) if (!fs.existsSync(file)) { - throwWithErrorText([ + printErrorMessagesAndExit([ `Schema file ${processOnlyThisOneSchemaFile} does not exist`, ]) } @@ -264,7 +290,7 @@ async function localSchemaFileAndTestFile( * @returns {boolean} */ const canThisTestBeRun = (jsonFilename) => { - if (!ignoreSkiptest && schemaValidation.skiptest.includes(jsonFilename)) { + if (!ignoreSkiptest && SchemaValidation.skiptest.includes(jsonFilename)) { return false // This test can be never process } if (fullScanAllFiles) { @@ -284,11 +310,11 @@ async function localSchemaFileAndTestFile( return } // Process all the schema files one by one via callback. - for (const schemaFileName of schemasToBeTested) { + for (const schemaFileName of SchemasToBeTested) { if (processOnlyThisOneSchemaFile) { if (schemaFileName !== processOnlyThisOneSchemaFile) return } - const schemaFullPathName = path.join(schemaDir, schemaFileName) + const schemaFullPathName = path.join(SchemaDir, schemaFileName) // Some schema files must be ignored. if ( @@ -302,7 +328,7 @@ async function localSchemaFileAndTestFile( try { jsonObj_ = buffer ? JSON.parse(buffer.toString()) : undefined } catch (err) { - throwWithErrorText([ + printErrorMessagesAndExit([ `JSON file ${schemaFullPathName} did not parse correctly.`, err, ]) @@ -335,7 +361,7 @@ async function localSchemaFileAndTestFile( try { return JSON.parse(buffer.toString()) } catch (err) { - throwWithErrorText([ + printErrorMessagesAndExit([ `JSON file ${testFileNameWithPath} did not parse correctly.`, err, ]) @@ -346,7 +372,7 @@ async function localSchemaFileAndTestFile( try { return YAML.parse(buffer.toString()) } catch (err) { - throwWithErrorText([ + printErrorMessagesAndExit([ `Can't read/decode yaml file: ${testFileNameWithPath}`, err, ]) @@ -361,14 +387,16 @@ async function localSchemaFileAndTestFile( joiner: '\n', }) } catch (err) { - throwWithErrorText([ + printErrorMessagesAndExit([ `Can't read/decode toml file: ${testFileNameWithPath}`, err, ]) } break default: - throwWithErrorText([`Unknown file extension: ${fileExtension}`]) + printErrorMessagesAndExit([ + `Unknown file extension: ${fileExtension}`, + ]) } } @@ -392,7 +420,7 @@ async function localSchemaFileAndTestFile( ) if (!filesInsideOneTestFolder.length) { - throwWithErrorText([ + printErrorMessagesAndExit([ `Found folder with no test files: ${folderNameAndPath}`, ]) } @@ -400,7 +428,7 @@ async function localSchemaFileAndTestFile( filesInsideOneTestFolder.forEach(function (testFileFullPathName) { // forbidden to add extra folder inside the specific test folder if (!fs.lstatSync(testFileFullPathName).isFile()) { - throwWithErrorText([ + printErrorMessagesAndExit([ `Found non test file inside test folder: ${testFileFullPathName}`, ]) } @@ -436,13 +464,13 @@ async function localSchemaFileAndTestFile( const schemaName = callbackParameterFromSchema.jsonName scanOneTestFolder( schemaName, - testPositiveDir, + TestPositiveDir, positiveTestScan, positiveTestScanDone, ) scanOneTestFolder( schemaName, - testNegativeDir, + TestNegativeDir, negativeTestScan, negativeTestScanDone, ) @@ -469,7 +497,7 @@ function testSchemaFileForBOM(schema) { (value, index) => buffer[index] === value, ) if (bomFound) { - throwWithErrorText([ + printErrorMessagesAndExit([ `Schema file must not have ${bom.name} BOM: ${schema.urlOrFilePath}`, ]) } @@ -574,7 +602,7 @@ function factoryAJV({ * @returns {getOptionReturn} */ function getOption(jsonName) { - const options = schemaValidation.options[jsonName] + const options = SchemaValidation.options[jsonName] // collect the unknownFormat list const unknownFormatsList = options?.unknownFormat ?? [] @@ -586,7 +614,7 @@ function getOption(jsonName) { const externalSchemaList = options?.externalSchema ?? [] const externalSchemaWithPathList = externalSchemaList?.map( (schemaFileName) => { - return path.resolve('.', schemaDir, schemaFileName) + return path.resolve('.', SchemaDir, schemaFileName) }, ) @@ -625,7 +653,7 @@ function ajv() { let schemaVersionStr = 'unknown' // const fullStrictMode = schemaValidation.ajvFullStrictMode.includes(schema.jsonName) // The SchemaStore default mode is full Strict Mode. Not in the list => full strict mode - const fullStrictMode = !schemaValidation.ajvNotStrictMode.includes( + const fullStrictMode = !SchemaValidation.ajvNotStrictMode.includes( schema.jsonName, ) const fullStrictModeStr = fullStrictMode @@ -659,13 +687,13 @@ function ajv() { // compile the schema validate = ajvSelected.compile(schemaJson) } catch (err) { - throwWithErrorText([ + printErrorMessagesAndExit([ `${textCompile}${schema.urlOrFilePath} (${schemaVersionStr})${fullStrictModeStr}`, err, ]) } countSchema++ - log.writeln() + console.log() log.ok( `${textPassSchema}${schema.urlOrFilePath} (${schemaVersionStr})${fullStrictModeStr}`, ) @@ -682,7 +710,7 @@ function ajv() { log.ok(`${textPositivePassTest}${schema.urlOrFilePath}`) }, () => { - throwWithErrorText([ + printErrorMessagesAndExit([ `${textPositiveFailedTest}${schema.urlOrFilePath}`, `(Schema file) keywordLocation: ${validate.errors[0].schemaPath}`, `(Test file) instanceLocation: ${validate.errors[0].instancePath}`, @@ -697,7 +725,7 @@ function ajv() { processTestFile( schema, () => { - throwWithErrorText([ + printErrorMessagesAndExit([ `${textNegativeFailedTest}${schema.urlOrFilePath}`, 'Negative test must always fail.', ]) @@ -716,8 +744,8 @@ function ajv() { } const processSchemaFileDone = () => { - log.writeln() - log.writeln(`Total schemas validated with AJV: ${countSchema}`) + console.log() + console.log(`Total schemas validated with AJV: ${countSchema}`) countSchema = 0 } @@ -785,14 +813,14 @@ async function taskNewSchema() { console.log('Enter the name of the schema (without .json extension)') await handleInput() - async function handleInput(schemaName) { + async function handleInput(/** @type {string | undefined} */ schemaName) { if (!schemaName || schemaName.endsWith('.json')) { rl.question('input: ', handleInput) return } - const schemaFile = path.join(schemaDir, schemaName + '.json') - const testDir = path.join(testPositiveDir, schemaName) + const schemaFile = path.join(SchemaDir, schemaName + '.json') + const testDir = path.join(TestPositiveDir, schemaName) const testFile = path.join(testDir, `${schemaName}.json`) if (fs.existsSync(schemaFile)) { @@ -832,46 +860,45 @@ async function taskNewSchema() { } } -function taskLint() { - lintSchemaHasCorrectMetadata() - lintTopLevelRefIsStandalone() - lintSchemaNoSmartQuotes() +async function taskLint() { + await lintSchemaHasCorrectMetadata() + await lintTopLevelRefIsStandalone() + await lintSchemaNoSmartQuotes() } async function taskCheck() { - // Check filesystem - assertDirectoryStructureIsValid() - assertFilenamesHaveCorrectExtensions() - assertTestFoldersHaveAtLeastOneTestSchema() - - // Check schema-validation.json - assertSchemaValidationJsonHasNoDuplicateItemsInLists() - assertSchemaValidationJsonHasNoMissingSchemaFiles() - assertSchemaValidationJsonHasNoUnmatchedUrls() - assertSchemaValidationJsonHasValidSkipTest() + // Check file system. + assertFsDirectoryStructureIsValid() + await assertFsFilenamesHaveCorrectExtensions() + assertFsTestFoldersHaveAtLeastOneTestSchema() - // Check catalog.json - await assertCatalogJsonPassesJsonLint() + // Check catalog.json. assertCatalogJsonValidatesAgainstJsonSchema() + await assertCatalogJsonPassesJsonLint() assertCatalogJsonHasNoDuplicateNames() - assertCatalogJsonHasNoPoorlyWordedFields() - assertCatalogJsonHasCorrectFileMatchPath() + assertCatalogJsonHasNoBadFields() assertCatalogJsonHasNoFileMatchConflict() assertCatalogJsonLocalUrlsMustRefFile() - assertCatalogJsonIncludesAllSchemas() + await assertCatalogJsonIncludesAllSchemas() - // Check test schema - assertSchemaHasNoBom() - assertSchemaHasNoDuplicatedPropertyKeys() - assertSchemaHasValidSchemaField() - assertSchemaHasValidIdField() - assertSchemaPassesSchemaSafeLint() + // Check schema-validation.json. + assertSchemaValidationJsonValidatesAgainstJsonSchema() + assertSchemaValidationJsonReferencesNoNonexistentFiles() + assertSchemaValidationJsonHasNoUnmatchedUrls() + assertSchemaValidationJsonHasValidSkipTest() + + // Check schemas. + await assertSchemaHasNoBom() + await assertSchemaHasNoDuplicatedPropertyKeys() + await assertSchemaHasValidSchemaField() + await assertSchemaHasValidIdField() + await assertSchemaPassesSchemaSafeLint() - printSchemasTestedInFullStrictMode() + await printSchemasTestedInFullStrictMode() printSchemasWithoutPositiveTestFiles() - testAjv() + await testAjv() printUrlCountsInCatalog() - printCountSchemaVersions() + await printCountSchemaVersions() } async function taskCheckRemote() { @@ -881,14 +908,14 @@ async function taskCheckRemote() { } async function taskMaintenance() { - printDowngradableSchemaVersions() - printStrictAndNotStrictAjvValidatedSchemas() + await printDowngradableSchemaVersions() + await printStrictAndNotStrictAjvValidatedSchemas() } async function taskCoverage() { const javaScriptCoverageName = 'schema.json.translated.to.js' const javaScriptCoverageNameWithPath = path.join( - `${temporaryCoverageDir}/${javaScriptCoverageName}`, + `${TempCoverageDir}/${javaScriptCoverageName}`, ) /** @@ -934,7 +961,7 @@ async function taskCoverage() { const ajvSelected = factoryAJV({ schemaName: versionObj?.schemaName, unknownFormatsList, - fullStrictMode: !schemaValidation.ajvNotStrictMode.includes(jsonName), + fullStrictMode: !SchemaValidation.ajvNotStrictMode.includes(jsonName), standAloneCode: true, standAloneCodeWithMultipleSchema: multipleSchema, }) @@ -953,7 +980,7 @@ async function taskCoverage() { ? mainSchema.id : mainSchema.$id if (!mainSchemaJsonId) { - throwWithErrorText([`Missing $id or id in ${jsonName}`]) + printErrorMessagesAndExit([`Missing $id or id in ${jsonName}`]) } moduleCode = AjvStandalone(ajvSelected) } else { @@ -966,7 +993,7 @@ async function taskCoverage() { const prettierOptions = await prettier.resolveConfig(process.cwd()) fs.writeFileSync( javaScriptCoverageNameWithPath, - prettier.format(moduleCode, { + await prettier.format(moduleCode, { ...prettierOptions, parser: 'babel', printWidth: 200, @@ -1002,17 +1029,19 @@ async function taskCoverage() { const schemaNameToBeCoverage = argv.SchemaName if (!schemaNameToBeCoverage) { - throwWithErrorText(['Must start "make" file with --SchemaName parameter.']) + printErrorMessagesAndExit([ + 'Must start "make" file with --SchemaName parameter.', + ]) } await generateCoverage(schemaNameToBeCoverage) log.ok('OK') } -function lintSchemaHasCorrectMetadata() { +async function lintSchemaHasCorrectMetadata() { let countScan = 0 let totalMismatchIds = 0 let totalIncorrectIds = 0 - localSchemaFileAndTestFile( + await localSchemaFileAndTestFile( { schemaOnlyScan(schema) { countScan++ @@ -1084,10 +1113,10 @@ function lintSchemaHasCorrectMetadata() { log.ok(`Total files scan: ${countScan}`) } -function lintSchemaNoSmartQuotes() { +async function lintSchemaNoSmartQuotes() { let countScan = 0 - localSchemaFileAndTestFile( + await localSchemaFileAndTestFile( { schemaOnlyScan(schema) { countScan++ @@ -1113,18 +1142,18 @@ function lintSchemaNoSmartQuotes() { { fullScanAllFiles: true, skipReadFile: false }, ) - log.writeln(`Total files scan: ${countScan}`) + console.log(`Total files scan: ${countScan}`) } -function lintTopLevelRefIsStandalone() { +async function lintTopLevelRefIsStandalone() { let countScan = 0 - localSchemaFileAndTestFile( + await localSchemaFileAndTestFile( { schemaOnlyScan(schema) { if (schema.jsonObj.$ref?.startsWith('http')) { for (const [member] of Object.entries(schema.jsonObj)) { if (member !== '$ref') { - throwWithErrorText([ + printErrorMessagesAndExit([ `Schemas that reference a remote schema must only have $ref as a property. Found property "${member}" for ${schema.jsonName}`, ]) } @@ -1140,9 +1169,9 @@ function lintTopLevelRefIsStandalone() { log.ok(`All urls tested OK. Total: ${countScan}`) } -function testAjv() { +async function testAjv() { const x = ajv() - localSchemaFileAndTestFile( + await localSchemaFileAndTestFile( { schemaForTestScan: x.testSchemaFile, positiveTestScan: x.positiveTestFile, @@ -1151,6 +1180,7 @@ function testAjv() { }, { skipReadFile: false }, ) + log.ok('local AJV schema passed') } @@ -1161,8 +1191,8 @@ async function remoteTestAjv() { x.testSchemaFile(testSchemaFile) countScan++ }) - log.writeln() - log.writeln(`Total schemas validated with AJV: ${countScan}`) + console.log() + console.log(`Total schemas validated with AJV: ${countScan}`) } async function remoteAssertSchemaHasNoBom() { @@ -1177,401 +1207,428 @@ async function remotePrintCountSchemaVersions() { x.process_data_done() } -async function assertCatalogJsonPassesJsonLint() { - jsonlint.parse( - await fs.promises.readFile('./src/api/json/catalog.json', 'utf-8'), +function assertFsTestFoldersHaveAtLeastOneTestSchema() { + const check = (/** @type {string[]} */ folderList) => { + for (const folderName of folderList) { + if (skipThisFileName(folderName)) { + continue + } + + if (!SchemasToBeTested.includes(folderName + '.json')) { + printErrorMessagesAndExit([ + `No schema ${folderName}.json found for test folder => ${folderName}`, + ]) + } + } + } + + check(FoldersPositiveTest) + check(FoldersNegativeTest) + + log.ok(`directories: test directories have at least one test schema`) +} + +async function assertFsFilenamesHaveCorrectExtensions() { + const schemaFileExtension = ['.json'] + const testFileExtension = ['.json', '.yml', '.yaml', '.toml'] + let countScan = 0 + + const x = (data, fileExtensionList) => { + countScan++ + const found = fileExtensionList.find((x) => data.jsonName.endsWith(x)) + if (!found) { + printErrorMessagesAndExit([ + `Filename must have ${fileExtensionList} extension => ${data.urlOrFilePath}`, + ]) + } + } + + await localSchemaFileAndTestFile( { + schemaForTestScan: (schema) => x(schema, schemaFileExtension), + positiveTestScan: (schema) => x(schema, testFileExtension), + negativeTestScan: (schema) => x(schema, testFileExtension), + }, + { + fullScanAllFiles: true, + }, + ) + + log.ok( + `All schema and test filename have the correct file extension. Total files scan: ${countScan}`, + ) +} + +function assertFsDirectoryStructureIsValid() { + SchemasToBeTested.forEach((name) => { + if ( + !skipThisFileName(name) && + !fs.lstatSync(path.join(SchemaDir, name)).isFile() + ) { + printErrorMessagesAndExit([ + `There can only be files in directory : ${SchemaDir} => ${name}`, + ]) + } + }) + + FoldersPositiveTest.forEach((name) => { + if ( + !skipThisFileName(name) && + !fs.lstatSync(path.join(TestPositiveDir, name)).isDirectory() + ) { + printErrorMessagesAndExit([ + `There can only be directory's in :${TestPositiveDir} => ${name}`, + ]) + } + }) + + FoldersNegativeTest.forEach((name) => { + if ( + !skipThisFileName(name) && + !fs.lstatSync(path.join(TestNegativeDir, name)).isDirectory() + ) { + printErrorMessagesAndExit([ + `There can only be directory's in :${TestNegativeDir} => ${name}`, + ]) + } + }) + + log.ok('directories: directory structure is valid') +} + +async function assertCatalogJsonPassesJsonLint() { + try { + jsonlint.parse(await fs.promises.readFile(CatalogFile, 'utf-8'), { ignoreBOM: false, ignoreComments: false, ignoreTrailingCommas: false, allowSingleQuotedStrings: false, allowDuplicateObjectKeys: false, - }, - ) + }) + log.ok('catalog.json: Parses with jsonlint') + } catch (err) { + printErrorMessagesAndExit( + [`Failed strict jsonlint parse of file "${CatalogFile}"`], + err.toString(), + ) + } } function assertCatalogJsonValidatesAgainstJsonSchema() { - const catalogSchema = readJsonFile( - path.join(schemaDir, 'schema-catalog.json'), - ) - const ajvInstance = factoryAJV({ schemaName: 'draft-04' }) - if (ajvInstance.validate(catalogSchema, catalog)) { - log.ok('catalog.json OK') + const catalogSchemaFile = path.join(SchemaDir, 'schema-catalog.json') + const catalogSchema = readJsonFile(catalogSchemaFile) + + const ajv = new AjvDraft06And07({ + strict: true, + }) + addFormats(ajv) + + if (ajv.validate(catalogSchema, Catalog)) { + log.ok('catalog.json: Validates against schema') } else { - throwWithErrorText([ - `(Schema file) keywordLocation: ${ajvInstance.errors[0].schemaPath}`, - `(Catalog file) instanceLocation: ${ajvInstance.errors[0].instancePath}`, - `(message) instanceLocation: ${ajvInstance.errors[0].message}`, - '"Catalog ERROR"', - ]) + printErrorMessagesAndExit( + [ + `Failed to validate file "${CatalogFile}" against schema file "./${catalogSchemaFile}"`, + `Showing first error out of ${ajv.errors?.length ?? '?'} total error(s)`, + ], + util.formatWithOptions({ colors: true }, '%O', ajv.errors?.[0] ?? '???'), + ) } } function assertCatalogJsonHasNoDuplicateNames() { /** @type {string[]} */ - const schemaNames = catalog.schemas.map((entry) => entry.name) + const schemaNames = Catalog.schemas.map((entry) => entry.name) /** @type {string[]} */ - const duplicateSchemaNames = [] - for (const schemaName of schemaNames) { - const matches = schemaNames.filter((item) => item === schemaName) - if (matches.length > 1 && !duplicateSchemaNames.includes(schemaName)) { - duplicateSchemaNames.push(schemaName) + for (const catalogEntry of Catalog.schemas) { + if ( + schemaNames.indexOf(catalogEntry.name) !== + schemaNames.lastIndexOf(catalogEntry.name) + ) { + printErrorMessagesAndExit([ + `Found two schema entries with duplicate "name" of "${catalogEntry.name}" in file "${CatalogFile}"`, + `Expected the "name" property of schema entries to be unique`, + ]) } } - - if (duplicateSchemaNames.length > 0) { - throwWithErrorText([ - `Found duplicates: ${JSON.stringify(duplicateSchemaNames)}`, - ]) - } } -function assertCatalogJsonHasNoPoorlyWordedFields() { - let countScan = 0 - - for (const entry of catalog.schemas) { +function assertCatalogJsonHasNoBadFields() { + for (const catalogEntry of Catalog.schemas) { if ( - schemaValidation.catalogEntryNoLintNameOrDescription.includes(entry.url) + SchemaValidation.catalogEntryNoLintNameOrDescription.includes( + catalogEntry.url, + ) ) { continue } - const schemaName = new URL(entry.url).pathname.slice(1) - for (const property of ['name', 'description']) { if ( - /$[,. \t-]/u.test(entry?.[property]) || - /[,. \t-]$/u.test(entry?.[property]) + /$[,. \t-]/u.test(catalogEntry?.[property]) || + /[,. \t-]$/u.test(catalogEntry?.[property]) ) { - ++countScan - - throwWithErrorText([ - `Catalog entry .${property}: Should not start or end with punctuation or whitespace (${schemaName})`, + printErrorMessagesAndExit([ + `Expected the "name" or "description" properties of catalog entries to not end with characters ",.-"`, + `The invalid entry has a "url" of "${catalogEntry.url}" in file "${CatalogFile}"`, ]) } } for (const property of ['name', 'description']) { - if (entry?.[property]?.toLowerCase()?.includes('schema')) { - ++countScan - - throwWithErrorText([ - `Catalog entry .${property}: Should not contain the string 'schema'. In most cases, this word is extraneous and the meaning is implied (${schemaName})`, + if (catalogEntry?.[property]?.toLowerCase()?.includes('schema')) { + printErrorMessagesAndExit([ + `Expected the "name" or "description" properties of catalog entries to not include the word "schema"`, + `All files are already schemas, so its meaning is implied`, + `If the JSON schema is actually a meta-schema (or some other exception applies), ignore this error by appending to the property "catalogEntryNoLintNameOrDescription" in file "${SchemaValidationFile}"`, + `The invalid entry has a "url" of "${catalogEntry.url}" in file "${CatalogFile}"`, ]) } } for (const property of ['name', 'description']) { - if (entry?.[property]?.toLowerCase()?.includes('\n')) { - ++countScan - - throwWithErrorText([ - `Catalog entry .${property}: Should not contain a newline character. In editors like VSCode, the newline is not rendered. (${schemaName})`, + if (catalogEntry?.[property]?.toLowerCase()?.includes('\n')) { + printErrorMessagesAndExit([ + `Expected the "name" or "description" properties of catalog entries to not include a newline character"`, + `The invalid entry has a "url" of "${catalogEntry.url}" in file "${CatalogFile}"`, ]) } } - } - - log.writeln(`Total found files: ${countScan}`) -} -function assertCatalogJsonHasCorrectFileMatchPath() { - for (const schema of catalog.schemas) { - schema.fileMatch?.forEach((fileMatchItem) => { - if (fileMatchItem.includes('/')) { + for (const fileGlob of catalogEntry.fileMatch ?? []) { + if (fileGlob.includes('/')) { // A folder must start with **/ - if (!fileMatchItem.startsWith('**/')) { - throwWithErrorText([ - `fileMatch with directory must start with "**/" => ${fileMatchItem}`, + if (!fileGlob.startsWith('**/')) { + printErrorMessagesAndExit([ + 'Expected the "fileMatch" values of catalog entries to start with "**/" if it matches a directory', + `The invalid entry has a "url" of "${catalogEntry.url}" in file "${CatalogFile}"`, ]) } } - }) + } } - log.ok('fileMatch path OK') + + log.ok(`catalog.json: Has no fields that break guidelines`) } function assertCatalogJsonHasNoFileMatchConflict() { - const fileMatchConflict = schemaValidation.fileMatchConflict - let fileMatchCollection = [] - // Collect all the "fileMatch" and put it in fileMatchCollection[] - for (const schema of catalog.schemas) { - const fileMatchArray = schema.fileMatch - if (fileMatchArray) { - // Check if this is already present in the "fileMatchConflict" list. If so then remove it from filtered[] - const filtered = fileMatchArray.filter((fileMatch) => { - return !fileMatchConflict.includes(fileMatch) - }) - // Check if fileMatch is already present in the fileMatchCollection[] - filtered.forEach((fileMatch) => { - if (fileMatchCollection.includes(fileMatch)) { - throwWithErrorText([`Duplicate fileMatch found => ${fileMatch}`]) - } - }) - fileMatchCollection = fileMatchCollection.concat(filtered) + const allFileMatches = [] + + for (const catalogEntry of Catalog.schemas) { + for (const fileGlob of catalogEntry.fileMatch ?? []) { + // Ignore globs that are OK to conflict for backwards compatibility. + if (SchemaValidation.fileMatchConflict.includes(fileGlob)) { + continue + } + + if (allFileMatches.includes(fileGlob)) { + printErrorMessagesAndExit([ + `Expected "fileMatch" value of "${fileGlob}" to be unique across all "fileMatch" properties in file "${CatalogFile}"`, + ]) + } + + allFileMatches.push(fileGlob) } } - log.ok('No new fileMatch conflict detected.') + + log.ok('catalog.json: No duplicate "fileMatch" values found') } function assertCatalogJsonLocalUrlsMustRefFile() { - const urlRecommendation = 'https://json.schemastore.org/.json' - let countScan = 0 - - getUrlFromCatalog((catalogUrl) => { - const SchemaStoreHost = 'json.schemastore.org' - // URL host that does not have SchemaStoreHost is an external schema.local_assert_catalog.json_local_url_must_ref_file - const URLcheck = new URL(catalogUrl) - if (!SchemaStoreHost.includes(URLcheck.host)) { - // This is an external schema. + getUrlFromCatalog((/** @type {string} */ catalogUrl) => { + // Skip external schemas from check. + if (!catalogUrl.startsWith(UrlSchemaStore)) { return } - countScan++ - // Check if local URLs have .json extension - const filenameMustBeAtThisUrlDepthPosition = 3 - const filename = catalogUrl.split('/')[filenameMustBeAtThisUrlDepthPosition] - if (!filename?.endsWith('.json')) { - throwWithErrorText([ - `Wrong: ${catalogUrl} Missing ".json" extension.`, - `Must be in this format: ${urlRecommendation}`, + + const filename = new URL(catalogUrl).pathname.slice(1) + + // Check that local URLs have end in .json + if (!filename.endsWith('.json')) { + printErrorMessagesAndExit([ + `Expected catalog entries for local files to have a "url" that ends in ".json"`, + `The invalid entry has a "url" of "${catalogUrl}" in file "${CatalogFile}"`, ]) } + // Check if schema file exist or not. - if (fs.existsSync(path.resolve('.', schemaDir, filename)) === false) { - throwWithErrorText([ - `The catalog have this URL: ${catalogUrl}`, - `But there is no schema file present: ${filename}`, + if (!fs.existsSync(path.join(SchemaDir, filename))) { + printErrorMessagesAndExit([ + `Expected schema file to exist at "${path.join(SchemaDir, filename)}", but no file found`, + `Schema file path inferred from catalog entry with a "url" of "${catalogUrl}" in file "${CatalogFile}"`, ]) } }) - log.ok(`All local url tested OK. Total: ${countScan}`) + + log.ok(`catalog.json: All local entries point to a schema file that exists`) } -function assertCatalogJsonIncludesAllSchemas() { - let countScan = 0 +async function assertCatalogJsonIncludesAllSchemas() { const allCatalogLocalJsonFiles = [] - // Read all the JSON file name from catalog and add it to allCatalogLocalJsonFiles[] - getUrlFromCatalog((catalogUrl) => { - // No need to validate the local URL correctness. It is already done in "local_assert_catalog.json_local_url_must_ref_file" - // Only scan for local schema. - if (catalogUrl.startsWith(urlSchemaStore)) { - const filename = catalogUrl.split('/').pop() + getUrlFromCatalog((/** @type {string} */ catalogUrl) => { + if (catalogUrl.startsWith(UrlSchemaStore)) { + const filename = new URL(catalogUrl).pathname.slice(1) allCatalogLocalJsonFiles.push(filename) } }) - // Check if allCatalogLocalJsonFiles[] have the actual schema filename. - const schemaFileCompare = (x) => { - // skip testing if present in "missingCatalogUrl" - if (!schemaValidation.missingCatalogUrl.includes(x.jsonName)) { - countScan++ - const found = allCatalogLocalJsonFiles.includes(x.jsonName) - if (!found) { - throwWithErrorText([ - 'Schema file name must be present in the catalog URL.', - `${x.jsonName} must be present in src/api/json/catalog.json`, - ]) - } - } - } - // Get all the JSON files for AJV - localSchemaFileAndTestFile( - { schemaOnlyScan: schemaFileCompare }, + await localSchemaFileAndTestFile( + { + schemaOnlyScan(schema) { + // Skip testing if schema is specified in "missingCatalogUrl". + if (SchemaValidation.missingCatalogUrl.includes(schema.jsonName)) { + return + } + + if (!allCatalogLocalJsonFiles.includes(schema.jsonName)) { + printErrorMessagesAndExit([ + `Expected schema file "${schema.jsonName}" to have a corresponding entry in the catalog file "${CatalogFile}"`, + `If this is intentional, ignore this error by appending to the property "missingCatalogUrl" in file "${SchemaValidationFile}"`, + ]) + } + }, + }, { fullScanAllFiles: true }, ) - log.ok(`All local schema files have URL link in catalog. Total: ${countScan}`) + + log.ok(`catalog.json: All local entries exist in file total`) } -function assertSchemaValidationJsonHasNoDuplicateItemsInLists() { - function checkForDuplicateInList(list, listName) { - if (list) { - if (new Set(list).size !== list.length) { - throwWithErrorText([`Duplicate item found in ${listName}`]) - } - } - } +function assertSchemaValidationJsonValidatesAgainstJsonSchema() { + const schemaValidationSchemaFile = './src/schema-validation.schema.json' + const schemaValidationSchema = readJsonFile(schemaValidationSchemaFile) - checkForDuplicateInList( - schemaValidation.ajvNotStrictMode, - 'ajvNotStrictMode[]', - ) - checkForDuplicateInList(schemaValidation.skiptest, 'skiptest[]') - checkForDuplicateInList( - schemaValidation.missingCatalogUrl, - 'missingCatalogUrl[]', - ) - checkForDuplicateInList( - schemaValidation.catalogEntryNoLintNameOrDescription, - 'catalogEntryNoLintNameOrDescription[]', - ) - checkForDuplicateInList( - schemaValidation.fileMatchConflict, - 'fileMatchConflict[]', - ) - checkForDuplicateInList( - schemaValidation.highSchemaVersion, - 'highSchemaVersion[]', - ) + const ajv = new AjvDraft06And07({ + strict: true, + }) + addFormats(ajv) - // Check for duplicate in options[] - const checkList = [] - for (const schemaName in schemaValidation.options) { - if (checkList.includes(schemaName)) { - throwWithErrorText([ - `Duplicate schema name found in options[] schema-validation.json => ${schemaName}`, - ]) - } - // Check for all values inside one option object - const optionValues = schemaValidation.options[schemaName] - checkForDuplicateInList( - optionValues?.unknownKeywords, - `${schemaName} unknownKeywords[]`, - ) - checkForDuplicateInList( - optionValues?.unknownFormat, - `${schemaName} unknownFormat[]`, - ) - checkForDuplicateInList( - optionValues?.externalSchema, - `${schemaName} externalSchema[]`, + if (ajv.validate(schemaValidationSchema, SchemaValidation)) { + log.ok('schema-validation.json: Validates against schema') + } else { + printErrorMessagesAndExit( + [ + `Failed to validate file "${SchemaValidationFile}" against schema file "${schemaValidationSchemaFile}"`, + `Showing first error out of ${ajv.errors?.length} total error(s)`, + ], + util.formatWithOptions({ colors: true }, '%O', ajv.errors?.[0] ?? '???'), ) - checkList.push(schemaName) } - - log.ok('OK') } -function assertSchemaValidationJsonHasNoMissingSchemaFiles() { - let countSchemaValidationItems = 0 - const x = (list) => { - list.forEach((schemaName) => { - if (schemaName.endsWith('.json')) { - countSchemaValidationItems++ - if (!schemasToBeTested.includes(schemaName)) { - throwWithErrorText([ - `No schema ${schemaName} found in schema folder => ${schemaDir}`, - ]) - } - } - }) - } - x(schemaValidation.ajvNotStrictMode) - x(schemaValidation.skiptest) - x(schemaValidation.missingCatalogUrl) - x(schemaValidation.highSchemaVersion) - - for (const schemaName in schemaValidation.options) { - if (schemaName !== 'readme_example.json') { - countSchemaValidationItems++ - if (!schemasToBeTested.includes(schemaName)) { - throwWithErrorText([ - `No schema ${schemaName} found in schema folder => ${schemaDir}`, +function assertSchemaValidationJsonReferencesNoNonexistentFiles() { + const check = ( + /** @type {string[]} */ schemaNames, + /** @type {string} */ propertyName, + ) => { + for (const schemaName of schemaNames) { + if (!SchemasToBeTested.includes(`${schemaName}`)) { + printErrorMessagesAndExit([ + `Expected to find file at path "${SchemaDir}/${schemaName}"`, + `Filename "${schemaName}" declared in file "${SchemaValidationFile}" under property "${propertyName}[]"`, ]) } } } - log.ok( - `Total schema-validation.json items check: ${countSchemaValidationItems}`, - ) -} -function assertSchemaValidationJsonHasNoUnmatchedUrls() { - let totalItems = 0 + check(SchemaValidation.ajvNotStrictMode, 'ajvNotStrictMode') + check(SchemaValidation.skiptest, 'skiptest') + check(SchemaValidation.missingCatalogUrl, 'missingCatalogUrl') + check(SchemaValidation.highSchemaVersion, 'highSchemaVersion') - const x = (/** @type {string[]} */ schemaUrls) => { - schemaUrls.forEach((schemaUrl) => { - ++totalItems + for (const schemaName in SchemaValidation.options) { + if (!SchemasToBeTested.includes(schemaName)) { + printErrorMessagesAndExit([ + `Expected to find file at path "${SchemaDir}/${schemaName}"`, + `Filename "${schemaName}" declared in file "${SchemaValidationFile}" under property "options"`, + ]) + } + } - const catalogUrls = catalog.schemas.map((item) => item.url) + log.ok('schema-validation.json: References no non-existent files') +} + +function assertSchemaValidationJsonHasNoUnmatchedUrls() { + const check = ( + /** @type {string[]} */ schemaUrls, + /** @type {string} */ propertyName, + ) => { + for (const schemaUrl of schemaUrls) { + const catalogUrls = Catalog.schemas.map((item) => item.url) if (!catalogUrls.includes(schemaUrl)) { - throwWithErrorText([ - `No schema with URL '${schemaUrl}' found in catalog.json`, + printErrorMessagesAndExit([ + `Failed to find a "url" with value of "${schemaUrl}" in file "${CatalogFile}" under property "${propertyName}[]"`, ]) } - }) + } } - x(schemaValidation.catalogEntryNoLintNameOrDescription) + check( + SchemaValidation.catalogEntryNoLintNameOrDescription, + 'catalogEntryNoLintNameOrDescription', + ) - log.ok(`Total schema-validation.json items checked: ${totalItems}`) + log.ok(`schema-validation.json: Has no unmatched URLs`) } function assertSchemaValidationJsonHasValidSkipTest() { - let countSchemaValidationItems = 0 - const x = (list, listName) => { - list.forEach((schemaName) => { - if (schemaName.endsWith('.json')) { - countSchemaValidationItems++ - if (schemaValidation.skiptest.includes(schemaName)) { - throwWithErrorText([ - `Disabled/skiptest[] schema: ${schemaName} found in => ${listName}[]`, - ]) - } - } - }) - } - x(schemaValidation.ajvNotStrictMode, 'ajvNotStrictMode') - x(schemaValidation.missingCatalogUrl, 'missingCatalogUrl') - x(schemaValidation.highSchemaVersion, 'highSchemaVersion') - - for (const schemaName in schemaValidation.options) { - if (schemaName !== 'readme_example.json') { - countSchemaValidationItems++ - if (schemaValidation.skiptest.includes(schemaName)) { - throwWithErrorText([ - `Disabled/skiptest[] schema: ${schemaName} found in => options[]`, + const check = ( + /** @type {string[]} */ schemaNames, + /** @type {string} */ propertyName, + ) => { + for (const schemaName of schemaNames) { + if (SchemaValidation.skiptest.includes(schemaName)) { + printErrorMessagesAndExit([ + `Did not expect to find filename "${schemaName}" in file "${SchemaValidationFile}" under property "${propertyName}[]"`, + `Because filename "${schemaName}" is listed under "skiptest", it should not be referenced anywhere else in the file`, ]) } } } - // Test folder must not exist if defined in skiptest[] - schemaValidation.skiptest.forEach((schemaName) => { - countSchemaValidationItems++ + check(SchemaValidation.ajvNotStrictMode, 'ajvNotStrictMode') + check(SchemaValidation.missingCatalogUrl, 'missingCatalogUrl') + check(SchemaValidation.highSchemaVersion, 'highSchemaVersion') + + for (const schemaName in SchemaValidation.options) { + if (SchemaValidation.skiptest.includes(schemaName)) { + printErrorMessagesAndExit([ + `Did not expect to find filename "${schemaName}" in file "${SchemaValidationFile}" under property "options"`, + `Because filename "${schemaName}" is listed under "skiptest", it should not be referenced anywhere else in the file`, + ]) + } + } - const folderName = schemaName.replace('.json', '') + // Test folder must not exist if defined in skiptest[] + for (const schemaName of SchemaValidation.skiptest) { + const folderName = schemaName.replace(/\.json$/, '') - if (foldersPositiveTest.includes(folderName)) { - throwWithErrorText([ - `Disabled/skiptest[] schema: ${schemaName} cannot have positive test folder`, + if (FoldersPositiveTest.includes(folderName)) { + printErrorMessagesAndExit([ + `Did not expect to find positive test directory at "${path.join(TestPositiveDir, folderName)}"`, + `Because filename "${schemaName}" is listed under "skiptest", it should not have any positive test files`, ]) } - if (foldersNegativeTest.includes(folderName)) { - throwWithErrorText([ - `Disabled/skiptest[] schema: ${schemaName} cannot have negative test folder`, + + if (FoldersNegativeTest.includes(folderName)) { + printErrorMessagesAndExit([ + `Did not expect to find negative test directory at "${path.join(TestNegativeDir, folderName)}"`, + `Because filename "${schemaName}" is listed under "skiptest", it should not have any negative test files`, ]) } - }) + } + log.ok( - `Total schema-validation.json items check: ${countSchemaValidationItems}`, + `schema-validation.json: Entries under skiptest[] are not used elsewhere`, ) } -function assertTestFoldersHaveAtLeastOneTestSchema() { - let countTestFolders = 0 - const x = (listFolders) => { - listFolders.forEach((folderName) => { - if (!skipThisFileName(folderName)) { - countTestFolders++ - if (!schemasToBeTested.includes(folderName + '.json')) { - throwWithErrorText([ - `No schema ${folderName}.json found for test folder => ${folderName}`, - ]) - } - } - }) - } - x(foldersPositiveTest) - x(foldersNegativeTest) - log.ok(`Total test folders: ${countTestFolders}`) -} - -function assertSchemaHasNoBom() { +async function assertSchemaHasNoBom() { let countScan = 0 - localSchemaFileAndTestFile( + await localSchemaFileAndTestFile( { schemaOnlyScan(schema) { countScan++ @@ -1586,7 +1643,7 @@ function assertSchemaHasNoBom() { ) } -function assertSchemaHasNoDuplicatedPropertyKeys() { +async function assertSchemaHasNoDuplicatedPropertyKeys() { let countScan = 0 const findDuplicatedProperty = (/** @type {Schema} */ schema) => { ++countScan @@ -1614,10 +1671,10 @@ function assertSchemaHasNoDuplicatedPropertyKeys() { }, ) } catch (err) { - throwWithErrorText([`Test file: ${schema.urlOrFilePath}`, err]) + printErrorMessagesAndExit([`Test file: ${schema.urlOrFilePath}`, err]) } } - localSchemaFileAndTestFile( + await localSchemaFileAndTestFile( { schemaForTestScan: findDuplicatedProperty, positiveTestScan: findDuplicatedProperty, @@ -1630,10 +1687,10 @@ function assertSchemaHasNoDuplicatedPropertyKeys() { ) } -function assertSchemaHasValidSchemaField() { +async function assertSchemaHasValidSchemaField() { let countScan = 0 - localSchemaFileAndTestFile( + await localSchemaFileAndTestFile( { schemaOnlyScan(schema) { countScan++ @@ -1642,18 +1699,18 @@ function assertSchemaHasValidSchemaField() { (schemaDialect) => schemaDialect.url, ) if (!validSchemas.includes(schema.jsonObj.$schema)) { - throwWithErrorText([ + printErrorMessagesAndExit([ `Schema file has invalid or missing '$schema' keyword => ${schema.jsonName}`, `Valid schemas: ${JSON.stringify(validSchemas)}`, ]) } - if (!schemaValidation.highSchemaVersion.includes(schema.jsonName)) { + if (!SchemaValidation.highSchemaVersion.includes(schema.jsonName)) { const tooHighSchemas = SCHEMA_DIALECTS.filter( (schemaDialect) => schemaDialect.isTooHigh, ).map((schemaDialect) => schemaDialect.url) if (tooHighSchemas.includes(schema.jsonObj.$schema)) { - throwWithErrorText([ + printErrorMessagesAndExit([ `Schema version is too high => in file ${schema.jsonName}`, `Schema version '${schema.jsonObj.$schema}' is not supported by many editors and IDEs`, `${schema.jsonName} must use a lower schema version.`, @@ -1671,10 +1728,10 @@ function assertSchemaHasValidSchemaField() { log.ok(`Total files scan: ${countScan}`) } -function assertSchemaHasValidIdField() { +async function assertSchemaHasValidIdField() { let countScan = 0 - localSchemaFileAndTestFile( + await localSchemaFileAndTestFile( { schemaOnlyScan(schema) { countScan++ @@ -1686,14 +1743,14 @@ function assertSchemaHasValidIdField() { ] if (schemasWithDollarlessId.includes(schema.jsonObj.$schema)) { if (schema.jsonObj.id === undefined) { - throwWithErrorText([ + printErrorMessagesAndExit([ `Missing property 'id' for schema 'src/schemas/json/${schema.jsonName}'`, ]) } schemaId = schema.jsonObj.id } else { if (schema.jsonObj.$id === undefined) { - throwWithErrorText([ + printErrorMessagesAndExit([ `Missing property '$id' for schema 'src/schemas/json/${schema.jsonName}'`, ]) } @@ -1704,7 +1761,7 @@ function assertSchemaHasValidIdField() { !schemaId.startsWith('https://') && !schemaId.startsWith('http://') ) { - throwWithErrorText([ + printErrorMessagesAndExit([ schemaId, `Schema id/$id must begin with 'https://' or 'http://' for schema 'src/schemas/json/${schema.jsonName}'`, ]) @@ -1720,12 +1777,12 @@ function assertSchemaHasValidIdField() { log.ok(`Total files scan: ${countScan}`) } -function assertSchemaPassesSchemaSafeLint() { +async function assertSchemaPassesSchemaSafeLint() { if (!argv.lint) { return } let countScan = 0 - localSchemaFileAndTestFile( + await localSchemaFileAndTestFile( { schemaOnlyScan(schema) { countScan++ @@ -1746,73 +1803,9 @@ function assertSchemaPassesSchemaSafeLint() { log.ok(`Total files scan: ${countScan}`) } -function assertFilenamesHaveCorrectExtensions() { - const schemaFileExtension = ['.json'] - const testFileExtension = ['.json', '.yml', '.yaml', '.toml'] - let countScan = 0 - const x = (data, fileExtensionList) => { - countScan++ - const found = fileExtensionList.find((x) => data.jsonName.endsWith(x)) - if (!found) { - throwWithErrorText([ - `Filename must have ${fileExtensionList} extension => ${data.urlOrFilePath}`, - ]) - } - } - localSchemaFileAndTestFile( - { - schemaForTestScan: (schema) => x(schema, schemaFileExtension), - positiveTestScan: (schema) => x(schema, testFileExtension), - negativeTestScan: (schema) => x(schema, testFileExtension), - }, - { - fullScanAllFiles: true, - }, - ) - log.ok( - `All schema and test filename have the correct file extension. Total files scan: ${countScan}`, - ) -} - -function assertDirectoryStructureIsValid() { - schemasToBeTested.forEach((name) => { - if ( - !skipThisFileName(name) && - !fs.lstatSync(path.join(schemaDir, name)).isFile() - ) { - throwWithErrorText([ - `There can only be files in directory : ${schemaDir} => ${name}`, - ]) - } - }) - - foldersPositiveTest.forEach((name) => { - if ( - !skipThisFileName(name) && - !fs.lstatSync(path.join(testPositiveDir, name)).isDirectory() - ) { - throwWithErrorText([ - `There can only be directory's in :${testPositiveDir} => ${name}`, - ]) - } - }) - - foldersNegativeTest.forEach((name) => { - if ( - !skipThisFileName(name) && - !fs.lstatSync(path.join(testNegativeDir, name)).isDirectory() - ) { - throwWithErrorText([ - `There can only be directory's in :${testNegativeDir} => ${name}`, - ]) - } - }) - log.ok('OK') -} - -function printCountSchemaVersions() { +async function printCountSchemaVersions() { const x = showSchemaVersions() - localSchemaFileAndTestFile( + await localSchemaFileAndTestFile( { schemaOnlyScan: x.process_data, schemaOnlyScanDone: x.process_data_done, @@ -1828,7 +1821,7 @@ function printUrlCountsInCatalog() { let countScanURLExternal = 0 let countScanURLInternal = 0 getUrlFromCatalog((catalogUrl) => { - catalogUrl.startsWith(urlSchemaStore) + catalogUrl.startsWith(UrlSchemaStore) ? countScanURLInternal++ : countScanURLExternal++ }) @@ -1841,7 +1834,7 @@ function printUrlCountsInCatalog() { log.ok(`${totalCount} Total URL`) } -function printStrictAndNotStrictAjvValidatedSchemas() { +async function printStrictAndNotStrictAjvValidatedSchemas() { const schemaVersion = showSchemaVersions() const schemaInFullStrictMode = [] const schemaInNotStrictMode = [] @@ -1887,16 +1880,16 @@ function printStrictAndNotStrictAjvValidatedSchemas() { } const listSchema = (mode, list) => { - log.writeln('------------------------------------') - log.writeln(`Schemas in ${mode} strict mode:`) + console.log('------------------------------------') + console.log(`Schemas in ${mode} strict mode:`) list.forEach((schemaName) => { // Write it is JSON list format. For easy copy to schema-validation.json - log.writeln(`"${schemaName}",`) + console.log(`"${schemaName}",`) }) log.ok(`Total schemas check ${mode} strict mode: ${list.length}`) } - localSchemaFileAndTestFile( + await localSchemaFileAndTestFile( { schemaOnlyScan: checkIfThisSchemaIsAlreadyInStrictMode, }, @@ -1905,8 +1898,8 @@ function printStrictAndNotStrictAjvValidatedSchemas() { listSchema('Full', schemaInFullStrictMode) listSchema('Not', schemaInNotStrictMode) - log.writeln() - log.writeln('------------------------------------') + console.log() + console.log('------------------------------------') log.ok( `Total all schemas check: ${ schemaInFullStrictMode.length + schemaInNotStrictMode.length @@ -1914,7 +1907,7 @@ function printStrictAndNotStrictAjvValidatedSchemas() { ) } -function printDowngradableSchemaVersions() { +async function printDowngradableSchemaVersions() { let countScan = 0 /** @@ -2004,21 +1997,21 @@ function printDowngradableSchemaVersions() { } } - log.writeln() + console.log() log.ok( 'Check if a lower $schema version will also pass the schema validation test', ) - localSchemaFileAndTestFile( + await localSchemaFileAndTestFile( { schemaOnlyScan: testLowerSchemaVersion }, { skipReadFile: false }, ) - log.writeln() + console.log() log.ok(`Total files scan: ${countScan}`) } -function printSchemasTestedInFullStrictMode() { +async function printSchemasTestedInFullStrictMode() { let countSchemaScanViaAJV = 0 - localSchemaFileAndTestFile({ + await localSchemaFileAndTestFile({ schemaOnlyScan() { countSchemaScanViaAJV++ }, @@ -2026,10 +2019,10 @@ function printSchemasTestedInFullStrictMode() { // If only ONE AJV schema test is run then this calculation does not work. if (countSchemaScanViaAJV !== 1) { const countFullStrictSchema = - countSchemaScanViaAJV - schemaValidation.ajvNotStrictMode.length + countSchemaScanViaAJV - SchemaValidation.ajvNotStrictMode.length const percent = (countFullStrictSchema / countSchemaScanViaAJV) * 100 log.ok( - 'Schema in full strict mode to prevent any unexpected behaviours or silently ignored mistakes in user schemas.', + 'Schema in full strict mode to prevent any unexpected behaviors or silently ignored mistakes in user schemas.', ) log.ok( `${countFullStrictSchema} of ${countSchemaScanViaAJV} (${Math.round( @@ -2042,16 +2035,16 @@ function printSchemasTestedInFullStrictMode() { function printSchemasWithoutPositiveTestFiles() { let countMissingTest = 0 // Check if each schemasToBeTested[] items is present in foldersPositiveTest[] - schemasToBeTested.forEach((schemaFileName) => { - if (!foldersPositiveTest.includes(schemaFileName.replace('.json', ''))) { + SchemasToBeTested.forEach((schemaName) => { + if (!FoldersPositiveTest.includes(schemaName.replace('.json', ''))) { countMissingTest++ - log.ok(`(No positive test file present): ${schemaFileName}`) + log.ok(`(No positive test file present): ${schemaName}`) } }) if (countMissingTest > 0) { - const percent = (countMissingTest / schemasToBeTested.length) * 100 - log.writeln() - log.writeln(`${Math.round(percent)}% of schemas do not have tests.`) + const percent = (countMissingTest / SchemasToBeTested.length) * 100 + console.log() + console.log(`${Math.round(percent)}% of schemas do not have tests.`) log.ok( `Schemas that have no positive test files. Total files: ${countMissingTest}`, ) diff --git a/src/api/json/catalog.json b/src/api/json/catalog.json index fa80c1d42b3..c86f79a1fa7 100644 --- a/src/api/json/catalog.json +++ b/src/api/json/catalog.json @@ -6,7 +6,7 @@ "name": "1Password SSH Agent Config", "description": "Configuration file for the 1Password SSH agent", "fileMatch": ["**/1password/ssh/agent.toml"], - "url": "https://developer.1password.com/schema/ssh-agent-config.json" + "url": "https://developer.1password.com/schema/ssh-agent-config" }, { "name": "Application Accelerator", diff --git a/src/schema-validation.jsonc b/src/schema-validation.jsonc index 5387853b568..4e01b1f0355 100644 --- a/src/schema-validation.jsonc +++ b/src/schema-validation.jsonc @@ -897,15 +897,6 @@ "rc3-request-0.0.3.json": { "externalSchema": ["rc3-auth-0.0.3.json"] }, - "readme_example.json": { - "externalSchema": ["external_schema.json", "other_schema.json"], - "unknownFormat": ["int32", "permission", "other_format"], - "unknownKeywords": [ - "x-intellij-language-injection", - "defaultSnippets", - "other_keywords" - ] - }, "replit.json": { "unknownKeywords": ["x-taplo", "x-taplo-info"] }, diff --git a/src/schema-validation.schema.json b/src/schema-validation.schema.json index 19f8cba47ff..a88ed83edb8 100644 --- a/src/schema-validation.schema.json +++ b/src/schema-validation.schema.json @@ -47,6 +47,7 @@ }, "catalogEntryNoLintNameOrDescription": { "description": "Disable checking of the .name and .description properties of the catalog.json entries that have the following .url's", + "type": "array", "uniqueItems": true, "items": { "type": "string", @@ -55,6 +56,7 @@ }, "options": { "type": "object", + "additionalProperties": false, "patternProperties": { "\\.json$": { "type": "object", @@ -77,7 +79,7 @@ } }, "externalSchema": { - "description": "External schema that readme_example.json $ref to", + "description": "External schemas used in '$ref's", "type": "array", "uniqueItems": true, "items": { diff --git a/src/schemas/json/schema-catalog.json b/src/schemas/json/schema-catalog.json index 93a165c3fff..fa853b74bab 100644 --- a/src/schemas/json/schema-catalog.json +++ b/src/schemas/json/schema-catalog.json @@ -1,7 +1,7 @@ { - "$schema": "http://json-schema.org/draft-04/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://json.schemastore.org/schema-catalog.json", "additionalProperties": false, - "id": "https://json.schemastore.org/schema-catalog.json", "properties": { "$schema": { "description": "Link to https://json.schemastore.org/schema-catalog.json", @@ -27,8 +27,7 @@ "url": { "description": "An absolute URL to the schema location", "type": "string", - "format": "uri", - "pattern": "^https://" + "format": "uri" }, "name": { "description": "The name of the schema",