Validate json settings with ajv
This commit is contained in:
parent
d8556a9b1e
commit
3e2ebb7797
5 changed files with 112 additions and 108 deletions
|
@ -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]);
|
||||||
}
|
}
|
||||||
|
return new Keymaps(entries);
|
||||||
let data: KeymapsJSON = {};
|
|
||||||
for (let key of Object.keys(json)) {
|
|
||||||
data[key] = operations.valueOf(json[key]);
|
|
||||||
}
|
|
||||||
return new Keymaps(data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
20
src/shared/settings/Validator.ts
Normal file
20
src/shared/settings/Validator.ts
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
Reference in a new issue