From 3e2ebb7797f6e12777b6da943765ff172bd179a9 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Tue, 3 Dec 2019 14:29:36 +0900 Subject: [PATCH] Validate json settings with ajv --- src/shared/settings/Blacklist.ts | 66 ++++++++++++------------------ src/shared/settings/Keymaps.ts | 29 +++++++++---- src/shared/settings/Properties.ts | 37 +++++++++-------- src/shared/settings/Search.ts | 68 ++++++++++++------------------- src/shared/settings/Validator.ts | 20 +++++++++ 5 files changed, 112 insertions(+), 108 deletions(-) create mode 100644 src/shared/settings/Validator.ts diff --git a/src/shared/settings/Blacklist.ts b/src/shared/settings/Blacklist.ts index 0cfbd71..201e7fc 100644 --- a/src/shared/settings/Blacklist.ts +++ b/src/shared/settings/Blacklist.ts @@ -1,4 +1,23 @@ import Key from './Key'; +import Validator from './Validator'; + +const ItemSchema = { + anyOf: [ + { type: 'string' }, + { + type: 'object', + properties: { + url: { type: 'string' }, + keys: { + type: 'array', + items: { type: 'string', minLength: 1 }, + minItems: 1, + } + }, + required: ['url', 'keys'], + } + ], +}; export type BlacklistItemJSON = string | { url: string, @@ -12,18 +31,6 @@ const regexFromWildcard = (pattern: string): RegExp => { return new RegExp(regexStr); }; -const isArrayOfString = (raw: any): boolean => { - if (!Array.isArray(raw)) { - return false; - } - for (let x of Array.from(raw)) { - if (typeof x !== 'string') { - return false; - } - } - return true; -}; - export class BlacklistItem { public readonly pattern: string; @@ -47,30 +54,11 @@ export class BlacklistItem { this.keyEntities = this.keys.map(Key.fromMapKey); } - static fromJSON(raw: any): BlacklistItem { - if (typeof raw === 'string') { - return new BlacklistItem(raw, false, []); - } else if (typeof raw === 'object' && raw !== null) { - if (!('url' in raw)) { - throw new TypeError( - `missing field "url" of blacklist item: ${JSON.stringify(raw)}`); - } - if (typeof raw.url !== 'string') { - throw new TypeError( - `invalid field "url" of blacklist item: ${JSON.stringify(raw)}`); - } - if (!('keys' in raw)) { - throw new TypeError( - `missing field "keys" of blacklist item: ${JSON.stringify(raw)}`); - } - if (!isArrayOfString(raw.keys)) { - throw new TypeError( - `invalid field "keys" of blacklist item: ${JSON.stringify(raw)}`); - } - return new BlacklistItem(raw.url as string, true, raw.keys as string[]); - } - throw new TypeError( - `invalid format of blacklist item: ${JSON.stringify(raw)}`); + static fromJSON(json: unknown): BlacklistItem { + let obj = new Validator(ItemSchema).validate(json); + return typeof obj === 'string' + ? new BlacklistItem(obj, false, []) + : new BlacklistItem(obj.url, true, obj.keys); } toJSON(): BlacklistItemJSON { @@ -103,11 +91,11 @@ export default class Blacklist { ) { } - static fromJSON(json: any): Blacklist { + static fromJSON(json: unknown): Blacklist { if (!Array.isArray(json)) { - throw new TypeError('blacklist is not an array: ' + JSON.stringify(json)); + throw new TypeError('blacklist is not an array'); } - let items = Array.from(json).map(item => BlacklistItem.fromJSON(item)); + let items = json.map(o => BlacklistItem.fromJSON(o)); return new Blacklist(items); } diff --git a/src/shared/settings/Keymaps.ts b/src/shared/settings/Keymaps.ts index a5558b0..7e510d1 100644 --- a/src/shared/settings/Keymaps.ts +++ b/src/shared/settings/Keymaps.ts @@ -1,4 +1,18 @@ import * as operations from '../operations'; +import Validator from './Validator'; + +const Schema = { + type: 'object', + patternProperties: { + '.*': { + type: 'object', + properties: { + type: { type: 'string' }, + }, + required: ['type'], + }, + } +}; export type KeymapsJSON = { [key: string]: operations.Operation }; @@ -8,16 +22,13 @@ export default class Keymaps { ) { } - static fromJSON(json: any): Keymaps { - if (typeof json !== 'object' || json === null) { - throw new TypeError('invalid keymaps type: ' + JSON.stringify(json)); - } - - let data: KeymapsJSON = {}; - for (let key of Object.keys(json)) { - data[key] = operations.valueOf(json[key]); + static fromJSON(json: unknown): Keymaps { + let obj = new Validator(Schema).validate(json); + let entries: KeymapsJSON = {}; + for (let key of Object.keys(obj)) { + entries[key] = operations.valueOf(obj[key]); } - return new Keymaps(data); + return new Keymaps(entries); } combine(other: Keymaps): Keymaps { diff --git a/src/shared/settings/Properties.ts b/src/shared/settings/Properties.ts index 63ff991..9cdaffe 100644 --- a/src/shared/settings/Properties.ts +++ b/src/shared/settings/Properties.ts @@ -1,3 +1,20 @@ +import Validator from './Validator'; + +const Schema = { + type: 'object', + properties: { + hintchars: { + type: 'string', + }, + smoothscroll: { + type: 'boolean', + }, + complete: { + type: 'string', + }, + }, +}; + export type PropertiesJSON = { hintchars?: string; smoothscroll?: boolean; @@ -65,23 +82,9 @@ export default class Properties { this.complete = complete || defaultValues.complete; } - static fromJSON(json: any): Properties { - let defNames: Set = new Set(defs.map(def => def.name)); - let unknownName = Object.keys(json).find(name => !defNames.has(name)); - if (unknownName) { - throw new TypeError(`Unknown property name: "${unknownName}"`); - } - - for (let def of defs) { - if (!Object.prototype.hasOwnProperty.call(json, def.name)) { - continue; - } - if (typeof json[def.name] !== def.type) { - throw new TypeError( - `property "${def.name}" is not ${def.type}`); - } - } - return new Properties(json); + static fromJSON(json: unknown): Properties { + let obj = new Validator(Schema).validate(json); + return new Properties(obj); } static types(): PropertyTypes { diff --git a/src/shared/settings/Search.ts b/src/shared/settings/Search.ts index 4580236..bdbe4a8 100644 --- a/src/shared/settings/Search.ts +++ b/src/shared/settings/Search.ts @@ -1,3 +1,22 @@ +import Validator from './Validator'; + +const Schema = { + type: 'object', + properties: { + default: { type: 'string' }, + engines: { + type: 'object', + propertyNames: { + pattern: '^[A-Za-z_][A-Za-z0-9_]+$', + }, + patternProperties: { + '.*': { type: 'string' }, + }, + }, + }, + required: ['default'], +}; + type Entries = { [name: string]: string }; export type SearchJSON = { @@ -12,19 +31,10 @@ export default class Search { ) { } - static fromJSON(json: any): Search { - let defaultEngine = Search.getStringField(json, 'default'); - let engines = Search.getObjectField(json, 'engines'); + static fromJSON(json: unknown): Search { + let obj = new Validator(Schema).validate(json); - for (let [name, url] of Object.entries(engines)) { - if ((/\s/).test(name)) { - throw new TypeError( - `While space in the search engine not allowed: "${name}"`); - } - if (typeof url !== 'string') { - throw new TypeError( - `Invalid type of value in filed "engines": ${JSON.stringify(json)}`); - } + for (let [name, url] of Object.entries(obj.engines)) { let matches = url.match(/{}/g); if (matches === null) { throw new TypeError(`No {}-placeholders in URL of "${name}"`); @@ -32,15 +42,11 @@ export default class Search { throw new TypeError(`Multiple {}-placeholders in URL of "${name}"`); } } - - if (!Object.keys(engines).includes(defaultEngine)) { - throw new TypeError(`Default engine "${defaultEngine}" not found`); + if (!Object.keys(obj.engines).includes(obj.default)) { + throw new TypeError(`Default engine "${obj.default}" not found`); } - return new Search( - json.default as string, - json.engines, - ); + return new Search(obj.default, obj.engines); } toJSON(): SearchJSON { @@ -49,28 +55,4 @@ export default class Search { engines: this.engines, }; } - - private static getStringField(json: any, name: string): string { - if (!Object.prototype.hasOwnProperty.call(json, name)) { - throw new TypeError( - `missing field "${name}" on search: ${JSON.stringify(json)}`); - } - if (typeof json[name] !== 'string') { - throw new TypeError( - `invalid type of filed "${name}" on search: ${JSON.stringify(json)}`); - } - return json[name]; - } - - private static getObjectField(json: any, name: string): Object { - if (!Object.prototype.hasOwnProperty.call(json, name)) { - throw new TypeError( - `missing field "${name}" on search: ${JSON.stringify(json)}`); - } - if (typeof json[name] !== 'object' || json[name] === null) { - throw new TypeError( - `invalid type of filed "${name}" on search: ${JSON.stringify(json)}`); - } - return json[name]; - } } diff --git a/src/shared/settings/Validator.ts b/src/shared/settings/Validator.ts new file mode 100644 index 0000000..6aac07f --- /dev/null +++ b/src/shared/settings/Validator.ts @@ -0,0 +1,20 @@ +import Ajv from 'ajv'; + +export default class Validator { + constructor( + private schema: object | boolean, + ) { + } + + validate(data: any): T { + let ajv = new Ajv(); + let valid = ajv.validate(this.schema, data); + if (!valid) { + let message = ajv.errors!! + .map(err => `'${err.dataPath}' of ${err.keyword} ${err.message}`) + .join('; '); + throw new TypeError(message); + } + return data as T; + } +}