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