Validate json settings with ajv

jh-changes
Shin'ya Ueoka 5 years ago
parent d8556a9b1e
commit 3e2ebb7797
  1. 66
      src/shared/settings/Blacklist.ts
  2. 29
      src/shared/settings/Keymaps.ts
  3. 37
      src/shared/settings/Properties.ts
  4. 68
      src/shared/settings/Search.ts
  5. 20
      src/shared/settings/Validator.ts

@ -1,4 +1,23 @@
import Key from './Key'; 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 | { export type BlacklistItemJSON = string | {
url: string, url: string,
@ -12,18 +31,6 @@ const regexFromWildcard = (pattern: string): RegExp => {
return new RegExp(regexStr); 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 { export class BlacklistItem {
public readonly pattern: string; public readonly pattern: string;
@ -47,30 +54,11 @@ export class BlacklistItem {
this.keyEntities = this.keys.map(Key.fromMapKey); this.keyEntities = this.keys.map(Key.fromMapKey);
} }
static fromJSON(raw: any): BlacklistItem { static fromJSON(json: unknown): BlacklistItem {
if (typeof raw === 'string') { let obj = new Validator<BlacklistItemJSON>(ItemSchema).validate(json);
return new BlacklistItem(raw, false, []); return typeof obj === 'string'
} else if (typeof raw === 'object' && raw !== null) { ? new BlacklistItem(obj, false, [])
if (!('url' in raw)) { : new BlacklistItem(obj.url, true, obj.keys);
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)}`);
} }
toJSON(): BlacklistItemJSON { toJSON(): BlacklistItemJSON {
@ -103,11 +91,11 @@ export default class Blacklist {
) { ) {
} }
static fromJSON(json: any): Blacklist { static fromJSON(json: unknown): Blacklist {
if (!Array.isArray(json)) { 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); return new Blacklist(items);
} }

@ -1,4 +1,18 @@
import * as operations from '../operations'; 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 }; export type KeymapsJSON = { [key: string]: operations.Operation };
@ -8,16 +22,13 @@ export default class Keymaps {
) { ) {
} }
static fromJSON(json: any): Keymaps { static fromJSON(json: unknown): Keymaps {
if (typeof json !== 'object' || json === null) { let obj = new Validator<KeymapsJSON>(Schema).validate(json);
throw new TypeError('invalid keymaps type: ' + JSON.stringify(json)); let entries: KeymapsJSON = {};
} for (let key of Object.keys(obj)) {
entries[key] = operations.valueOf(obj[key]);
let data: KeymapsJSON = {};
for (let key of Object.keys(json)) {
data[key] = operations.valueOf(json[key]);
} }
return new Keymaps(data); return new Keymaps(entries);
} }
combine(other: Keymaps): Keymaps { combine(other: Keymaps): Keymaps {

@ -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 = { export type PropertiesJSON = {
hintchars?: string; hintchars?: string;
smoothscroll?: boolean; smoothscroll?: boolean;
@ -65,23 +82,9 @@ export default class Properties {
this.complete = complete || defaultValues.complete; this.complete = complete || defaultValues.complete;
} }
static fromJSON(json: any): Properties { static fromJSON(json: unknown): Properties {
let defNames: Set<string> = new Set(defs.map(def => def.name)); let obj = new Validator<PropertiesJSON>(Schema).validate(json);
let unknownName = Object.keys(json).find(name => !defNames.has(name)); return new Properties(obj);
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 types(): PropertyTypes { static types(): PropertyTypes {

@ -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 }; type Entries = { [name: string]: string };
export type SearchJSON = { export type SearchJSON = {
@ -12,19 +31,10 @@ export default class Search {
) { ) {
} }
static fromJSON(json: any): Search { static fromJSON(json: unknown): Search {
let defaultEngine = Search.getStringField(json, 'default'); let obj = new Validator<SearchJSON>(Schema).validate(json);
let engines = Search.getObjectField(json, 'engines');
for (let [name, url] of Object.entries(engines)) { for (let [name, url] of Object.entries(obj.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)}`);
}
let matches = url.match(/{}/g); let matches = url.match(/{}/g);
if (matches === null) { if (matches === null) {
throw new TypeError(`No {}-placeholders in URL of "${name}"`); 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}"`); throw new TypeError(`Multiple {}-placeholders in URL of "${name}"`);
} }
} }
if (!Object.keys(obj.engines).includes(obj.default)) {
if (!Object.keys(engines).includes(defaultEngine)) { throw new TypeError(`Default engine "${obj.default}" not found`);
throw new TypeError(`Default engine "${defaultEngine}" not found`);
} }
return new Search( return new Search(obj.default, obj.engines);
json.default as string,
json.engines,
);
} }
toJSON(): SearchJSON { toJSON(): SearchJSON {
@ -49,28 +55,4 @@ export default class Search {
engines: this.engines, 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];
}
} }

@ -0,0 +1,20 @@
import Ajv from 'ajv';
export default class Validator<T> {
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;
}
}