Merge pull request #684 from ueokande/jsonschema-settings

Parse settings by JSON Schema
jh-changes
Shin'ya Ueoka 5 years ago committed by GitHub
commit 3c7230c303
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      .circleci/config.yml
  2. 182
      package-lock.json
  3. 5
      package.json
  4. 47
      src/shared/settings/Blacklist.ts
  5. 22
      src/shared/settings/Keymaps.ts
  6. 18
      src/shared/settings/Properties.ts
  7. 50
      src/shared/settings/Search.ts
  8. 50
      src/shared/settings/Settings.ts
  9. 84
      src/shared/settings/schema.json
  10. 567
      src/shared/settings/validate.js
  11. 18
      test/shared/settings/Blacklist.test.ts
  12. 1
      test/shared/settings/Keymaps.test.ts
  13. 13
      test/shared/settings/Search.test.ts
  14. 2
      tsconfig.json
  15. 2
      webpack.config.js

@ -63,13 +63,6 @@ jobs:
- checkout - checkout
- setup_npm - setup_npm
- run: npm run lint - run: npm run lint
- run:
# NOTE: Karma loads ts-node automatically and treats karma.conf.js as a TypeScript.
# Karma does not starts by karma.conf.js transpile failure, and this hack removes
# ts-node module from the local before test.
# See: https://github.com/karma-runner/karma/issues/3329
name: Remove node-ts from node_modules
command: mv node_modules/ts-node node_modules/ts-node.orig
- run: npm test - run: npm test
- run: npm run package - run: npm run package

182
package-lock.json generated

@ -581,9 +581,9 @@
"dev": true "dev": true
}, },
"ajv": { "ajv": {
"version": "6.10.0", "version": "6.10.2",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz",
"integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==",
"dev": true, "dev": true,
"requires": { "requires": {
"fast-deep-equal": "^2.0.1", "fast-deep-equal": "^2.0.1",
@ -592,6 +592,28 @@
"uri-js": "^4.2.2" "uri-js": "^4.2.2"
} }
}, },
"ajv-cli": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ajv-cli/-/ajv-cli-3.0.0.tgz",
"integrity": "sha1-WCMjH2TigzBUEwaQsYCZYJ6YDyk=",
"dev": true,
"requires": {
"ajv": "^6.0.0",
"ajv-pack": "^0.3.0",
"fast-json-patch": "^0.5.6",
"glob": "^7.0.3",
"json-schema-migrate": "^0.2.0",
"minimist": "^1.2.0"
},
"dependencies": {
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true
}
}
},
"ajv-errors": { "ajv-errors": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz",
@ -604,6 +626,16 @@
"integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=", "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=",
"dev": true "dev": true
}, },
"ajv-pack": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/ajv-pack/-/ajv-pack-0.3.1.tgz",
"integrity": "sha1-tyxNQhnjko5ihC10Le2Tv1B5ZWA=",
"dev": true,
"requires": {
"js-beautify": "^1.6.4",
"require-from-string": "^1.2.0"
}
},
"amdefine": { "amdefine": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
@ -1576,6 +1608,12 @@
} }
} }
}, },
"co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
"integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=",
"dev": true
},
"code-point-at": { "code-point-at": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
@ -1670,6 +1708,16 @@
"typedarray": "^0.0.6" "typedarray": "^0.0.6"
} }
}, },
"config-chain": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz",
"integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==",
"dev": true,
"requires": {
"ini": "^1.3.4",
"proto-list": "~1.2.1"
}
},
"connect": { "connect": {
"version": "3.7.0", "version": "3.7.0",
"resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz",
@ -2226,6 +2274,36 @@
"safe-buffer": "^5.0.1" "safe-buffer": "^5.0.1"
} }
}, },
"editorconfig": {
"version": "0.15.3",
"resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz",
"integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==",
"dev": true,
"requires": {
"commander": "^2.19.0",
"lru-cache": "^4.1.5",
"semver": "^5.6.0",
"sigmund": "^1.0.1"
},
"dependencies": {
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
},
"lru-cache": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
"integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
"dev": true,
"requires": {
"pseudomap": "^1.0.2",
"yallist": "^2.1.2"
}
}
}
},
"ee-first": { "ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -3077,6 +3155,12 @@
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
"dev": true "dev": true
}, },
"fast-json-patch": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-0.5.7.tgz",
"integrity": "sha1-taj0nSWWJFlu+YuHLz/aiVtNhmU=",
"dev": true
},
"fast-json-stable-stringify": { "fast-json-stable-stringify": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
@ -4946,6 +5030,45 @@
"integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==", "integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==",
"dev": true "dev": true
}, },
"js-beautify": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.10.2.tgz",
"integrity": "sha512-ZtBYyNUYJIsBWERnQP0rPN9KjkrDfJcMjuVGcvXOUJrD1zmOGwhRwQ4msG+HJ+Ni/FA7+sRQEMYVzdTQDvnzvQ==",
"dev": true,
"requires": {
"config-chain": "^1.1.12",
"editorconfig": "^0.15.3",
"glob": "^7.1.3",
"mkdirp": "~0.5.1",
"nopt": "~4.0.1"
},
"dependencies": {
"glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"nopt": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz",
"integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=",
"dev": true,
"requires": {
"abbrev": "1",
"osenv": "^0.1.4"
}
}
}
},
"js-tokens": { "js-tokens": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
@ -4980,6 +5103,41 @@
"integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
"dev": true "dev": true
}, },
"json-schema-migrate": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/json-schema-migrate/-/json-schema-migrate-0.2.0.tgz",
"integrity": "sha1-ukelsAcvxyOWRg4b1gtE1SF4u8Y=",
"dev": true,
"requires": {
"ajv": "^5.0.0"
},
"dependencies": {
"ajv": {
"version": "5.5.2",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
"integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
"dev": true,
"requires": {
"co": "^4.6.0",
"fast-deep-equal": "^1.0.0",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.3.0"
}
},
"fast-deep-equal": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz",
"integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=",
"dev": true
},
"json-schema-traverse": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz",
"integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=",
"dev": true
}
}
},
"json-schema-traverse": { "json-schema-traverse": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@ -7357,6 +7515,12 @@
} }
} }
}, },
"proto-list": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
"integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=",
"dev": true
},
"proxy-addr": { "proxy-addr": {
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz",
@ -7884,6 +8048,12 @@
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
"dev": true "dev": true
}, },
"require-from-string": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-1.2.1.tgz",
"integrity": "sha1-UpyczvJzgK3+yaL5ZbZJu+5jZBg=",
"dev": true
},
"require-main-filename": { "require-main-filename": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
@ -8339,6 +8509,12 @@
"integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
"dev": true "dev": true
}, },
"sigmund": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz",
"integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=",
"dev": true
},
"signal-exit": { "signal-exit": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",

@ -2,10 +2,11 @@
"name": "vim-vixen", "name": "vim-vixen",
"description": "Vim vixen", "description": "Vim vixen",
"scripts": { "scripts": {
"schema": "ajv compile -s src/shared/settings/schema.json -o src/shared/settings/validate.js",
"start": "webpack --mode development -w --debug --devtool inline-source-map", "start": "webpack --mode development -w --debug --devtool inline-source-map",
"build": "NODE_ENV=production webpack --mode production --progress --display-error-details --devtool inline-source-map", "build": "NODE_ENV=production webpack --mode production --progress --display-error-details --devtool inline-source-map",
"package": "npm run build && script/package", "package": "npm run build && script/package",
"lint": "eslint --ext .js,.jsx,.ts,.tsx src", "lint": "eslint --ext .ts,.tsx src",
"type-checks": "tsc --noEmit", "type-checks": "tsc --noEmit",
"test": "karma start", "test": "karma start",
"test:e2e": "mocha --timeout 10000 --retries 10 --require ts-node/register --extension ts e2e" "test:e2e": "mocha --timeout 10000 --retries 10 --require ts-node/register --extension ts e2e"
@ -35,6 +36,8 @@
"@types/sinon": "^7.0.13", "@types/sinon": "^7.0.13",
"@typescript-eslint/eslint-plugin": "^2.0.0", "@typescript-eslint/eslint-plugin": "^2.0.0",
"@typescript-eslint/parser": "^2.0.0", "@typescript-eslint/parser": "^2.0.0",
"ajv": "^6.10.2",
"ajv-cli": "^3.0.0",
"chai": "^4.2.0", "chai": "^4.2.0",
"css-loader": "^3.2.0", "css-loader": "^3.2.0",
"eslint": "^6.2.2", "eslint": "^6.2.2",

@ -12,18 +12,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 +35,10 @@ 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: BlacklistItemJSON): BlacklistItem {
if (typeof raw === 'string') { return typeof json === 'string'
return new BlacklistItem(raw, false, []); ? new BlacklistItem(json, false, [])
} else if (typeof raw === 'object' && raw !== null) { : new BlacklistItem(json.url, true, json.keys);
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)}`);
} }
toJSON(): BlacklistItemJSON { toJSON(): BlacklistItemJSON {
@ -103,11 +71,8 @@ export default class Blacklist {
) { ) {
} }
static fromJSON(json: any): Blacklist { static fromJSON(json: BlacklistJSON): Blacklist {
if (!Array.isArray(json)) { let items = json.map(o => BlacklistItem.fromJSON(o));
throw new TypeError('blacklist is not an array: ' + JSON.stringify(json));
}
let items = Array.from(json).map(item => BlacklistItem.fromJSON(item));
return new Blacklist(items); return new Blacklist(items);
} }

@ -1,23 +1,25 @@
import * as operations from '../operations'; import * as operations from '../operations';
export type KeymapsJSON = { [key: string]: operations.Operation }; type OperationJson = {
type: string
} | {
type: string;
[prop: string]: string | number | boolean;
};
export type KeymapsJSON = { [key: string]: OperationJson };
export default class Keymaps { export default class Keymaps {
constructor( constructor(
private readonly data: KeymapsJSON, private readonly data: { [key: string]: operations.Operation },
) { ) {
} }
static fromJSON(json: any): Keymaps { static fromJSON(json: KeymapsJSON): Keymaps {
if (typeof json !== 'object' || json === null) { let entries: { [key: string]: operations.Operation } = {};
throw new TypeError('invalid keymaps type: ' + JSON.stringify(json));
}
let data: KeymapsJSON = {};
for (let key of Object.keys(json)) { for (let key of Object.keys(json)) {
data[key] = operations.valueOf(json[key]); entries[key] = operations.valueOf(json[key]);
} }
return new Keymaps(data); return new Keymaps(entries);
} }
combine(other: Keymaps): Keymaps { combine(other: Keymaps): Keymaps {

@ -1,3 +1,4 @@
export type PropertiesJSON = { export type PropertiesJSON = {
hintchars?: string; hintchars?: string;
smoothscroll?: boolean; smoothscroll?: boolean;
@ -65,22 +66,7 @@ export default class Properties {
this.complete = complete || defaultValues.complete; this.complete = complete || defaultValues.complete;
} }
static fromJSON(json: any): Properties { static fromJSON(json: PropertiesJSON): Properties {
let defNames: Set<string> = 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); return new Properties(json);
} }

@ -12,18 +12,10 @@ export default class Search {
) { ) {
} }
static fromJSON(json: any): Search { static fromJSON(json: SearchJSON): Search {
let defaultEngine = Search.getStringField(json, 'default'); for (let [name, url] of Object.entries(json.engines)) {
let engines = Search.getObjectField(json, 'engines'); if (!(/^[a-zA-Z0-9]+$/).test(name)) {
throw new TypeError('Search engine\'s name must be [a-zA-Z0-9]+');
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)}`);
} }
let matches = url.match(/{}/g); let matches = url.match(/{}/g);
if (matches === null) { if (matches === null) {
@ -32,15 +24,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(json.engines).includes(json.default)) {
if (!Object.keys(engines).includes(defaultEngine)) { throw new TypeError(`Default engine "${json.default}" not found`);
throw new TypeError(`Default engine "${defaultEngine}" not found`);
} }
return new Search( return new Search(json.default, json.engines);
json.default as string,
json.engines,
);
} }
toJSON(): SearchJSON { toJSON(): SearchJSON {
@ -49,28 +37,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];
}
} }

@ -1,13 +1,16 @@
import Ajv from 'ajv';
import Keymaps, { KeymapsJSON } from './Keymaps'; import Keymaps, { KeymapsJSON } from './Keymaps';
import Search, { SearchJSON } from './Search'; import Search, { SearchJSON } from './Search';
import Properties, { PropertiesJSON } from './Properties'; import Properties, { PropertiesJSON } from './Properties';
import Blacklist, { BlacklistJSON } from './Blacklist'; import Blacklist, { BlacklistJSON } from './Blacklist';
import validate from './validate';
export type SettingsJSON = { export type SettingsJSON = {
keymaps: KeymapsJSON, keymaps?: KeymapsJSON,
search: SearchJSON, search?: SearchJSON,
properties: PropertiesJSON, properties?: PropertiesJSON,
blacklist: BlacklistJSON, blacklist?: BlacklistJSON,
}; };
export default class Settings { export default class Settings {
@ -36,25 +39,30 @@ export default class Settings {
this.blacklist = blacklist; this.blacklist = blacklist;
} }
static fromJSON(json: any): Settings { static fromJSON(json: unknown): Settings {
let valid = validate(json);
if (!valid) {
let message = (validate as any).errors!!
.map((err: Ajv.ErrorObject) => {
return `'${err.dataPath}' ${err.message}`;
})
.join('; ');
throw new TypeError(message);
}
let obj = json as SettingsJSON;
let settings = { ...DefaultSetting }; let settings = { ...DefaultSetting };
for (let key of Object.keys(json)) { if (obj.keymaps) {
switch (key) { settings.keymaps = Keymaps.fromJSON(obj.keymaps);
case 'keymaps': }
settings.keymaps = Keymaps.fromJSON(json.keymaps); if (obj.search) {
break; settings.search = Search.fromJSON(obj.search);
case 'search': }
settings.search = Search.fromJSON(json.search); if (obj.properties) {
break; settings.properties = Properties.fromJSON(obj.properties);
case 'properties':
settings.properties = Properties.fromJSON(json.properties);
break;
case 'blacklist':
settings.blacklist = Blacklist.fromJSON(json.blacklist);
break;
default:
throw new TypeError('unknown setting: ' + key);
} }
if (obj.blacklist) {
settings.blacklist = Blacklist.fromJSON(obj.blacklist);
} }
return new Settings(settings); return new Settings(settings);
} }

@ -0,0 +1,84 @@
{
"type": "object",
"properties": {
"keymaps": {
"type": "object",
"patternProperties": {
".*": {
"type": "object",
"properties": {
"type": {
"type": "string"
}
},
"required": [
"type"
]
}
}
},
"search": {
"type": "object",
"properties": {
"default": {
"type": "string"
},
"engines": {
"type": "object",
"patternProperties": {
".*": {
"type": "string"
}
}
}
},
"required": [
"default",
"engines"
]
},
"properties": {
"type": "object",
"properties": {
"hintchars": {
"type": "string"
},
"smoothscroll": {
"type": "boolean"
},
"complete": {
"type": "string"
}
}
},
"blacklist": {
"type": "array",
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {
"url": {
"type": "string"
},
"keys": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"url",
"keys"
]
}
]
}
}
},
"additionalProperties": false
}

@ -0,0 +1,567 @@
'use strict';
var validate = (function() {
var pattern0 = new RegExp('.*');
var refVal = [];
return function validate(data, dataPath, parentData, parentDataProperty, rootData) {
'use strict';
var vErrors = null;
var errors = 0;
if ((data && typeof data === "object" && !Array.isArray(data))) {
var errs__0 = errors;
var valid1 = true;
for (var key0 in data) {
var isAdditional0 = !(false || key0 == 'keymaps' || key0 == 'search' || key0 == 'properties' || key0 == 'blacklist');
if (isAdditional0) {
valid1 = false;
validate.errors = [{
keyword: 'additionalProperties',
dataPath: (dataPath || '') + "",
schemaPath: '#/additionalProperties',
params: {
additionalProperty: '' + key0 + ''
},
message: 'should NOT have additional properties'
}];
return false;
break;
}
}
if (valid1) {
var data1 = data.keymaps;
if (data1 === undefined) {
valid1 = true;
} else {
var errs_1 = errors;
if ((data1 && typeof data1 === "object" && !Array.isArray(data1))) {
var errs__1 = errors;
var valid2 = true;
for (var key1 in data1) {
if (pattern0.test(key1)) {
var data2 = data1[key1];
var errs_2 = errors;
if ((data2 && typeof data2 === "object" && !Array.isArray(data2))) {
if (true) {
var errs__2 = errors;
var valid3 = true;
if (data2.type === undefined) {
valid3 = false;
validate.errors = [{
keyword: 'required',
dataPath: (dataPath || '') + '.keymaps[\'' + key1 + '\']',
schemaPath: '#/properties/keymaps/patternProperties/.*/required',
params: {
missingProperty: 'type'
},
message: 'should have required property \'type\''
}];
return false;
} else {
var errs_3 = errors;
if (typeof data2.type !== "string") {
validate.errors = [{
keyword: 'type',
dataPath: (dataPath || '') + '.keymaps[\'' + key1 + '\'].type',
schemaPath: '#/properties/keymaps/patternProperties/.*/properties/type/type',
params: {
type: 'string'
},
message: 'should be string'
}];
return false;
}
var valid3 = errors === errs_3;
}
}
} else {
validate.errors = [{
keyword: 'type',
dataPath: (dataPath || '') + '.keymaps[\'' + key1 + '\']',
schemaPath: '#/properties/keymaps/patternProperties/.*/type',
params: {
type: 'object'
},
message: 'should be object'
}];
return false;
}
var valid2 = errors === errs_2;
if (!valid2) break;
} else valid2 = true;
}
} else {
validate.errors = [{
keyword: 'type',
dataPath: (dataPath || '') + '.keymaps',
schemaPath: '#/properties/keymaps/type',
params: {
type: 'object'
},
message: 'should be object'
}];
return false;
}
var valid1 = errors === errs_1;
}
if (valid1) {
var data1 = data.search;
if (data1 === undefined) {
valid1 = true;
} else {
var errs_1 = errors;
if ((data1 && typeof data1 === "object" && !Array.isArray(data1))) {
if (true) {
var errs__1 = errors;
var valid2 = true;
if (data1.default === undefined) {
valid2 = false;
validate.errors = [{
keyword: 'required',
dataPath: (dataPath || '') + '.search',
schemaPath: '#/properties/search/required',
params: {
missingProperty: 'default'
},
message: 'should have required property \'default\''
}];
return false;
} else {
var errs_2 = errors;
if (typeof data1.default !== "string") {
validate.errors = [{
keyword: 'type',
dataPath: (dataPath || '') + '.search.default',
schemaPath: '#/properties/search/properties/default/type',
params: {
type: 'string'
},
message: 'should be string'
}];
return false;
}
var valid2 = errors === errs_2;
}
if (valid2) {
var data2 = data1.engines;
if (data2 === undefined) {
valid2 = false;
validate.errors = [{
keyword: 'required',
dataPath: (dataPath || '') + '.search',
schemaPath: '#/properties/search/required',
params: {
missingProperty: 'engines'
},
message: 'should have required property \'engines\''
}];
return false;
} else {
var errs_2 = errors;
if ((data2 && typeof data2 === "object" && !Array.isArray(data2))) {
var errs__2 = errors;
var valid3 = true;
for (var key2 in data2) {
if (pattern0.test(key2)) {
var errs_3 = errors;
if (typeof data2[key2] !== "string") {
validate.errors = [{
keyword: 'type',
dataPath: (dataPath || '') + '.search.engines[\'' + key2 + '\']',
schemaPath: '#/properties/search/properties/engines/patternProperties/.*/type',
params: {
type: 'string'
},
message: 'should be string'
}];
return false;
}
var valid3 = errors === errs_3;
if (!valid3) break;
} else valid3 = true;
}
} else {
validate.errors = [{
keyword: 'type',
dataPath: (dataPath || '') + '.search.engines',
schemaPath: '#/properties/search/properties/engines/type',
params: {
type: 'object'
},
message: 'should be object'
}];
return false;
}
var valid2 = errors === errs_2;
}
}
}
} else {
validate.errors = [{
keyword: 'type',
dataPath: (dataPath || '') + '.search',
schemaPath: '#/properties/search/type',
params: {
type: 'object'
},
message: 'should be object'
}];
return false;
}
var valid1 = errors === errs_1;
}
if (valid1) {
var data1 = data.properties;
if (data1 === undefined) {
valid1 = true;
} else {
var errs_1 = errors;
if ((data1 && typeof data1 === "object" && !Array.isArray(data1))) {
var errs__1 = errors;
var valid2 = true;
if (data1.hintchars === undefined) {
valid2 = true;
} else {
var errs_2 = errors;
if (typeof data1.hintchars !== "string") {
validate.errors = [{
keyword: 'type',
dataPath: (dataPath || '') + '.properties.hintchars',
schemaPath: '#/properties/properties/properties/hintchars/type',
params: {
type: 'string'
},
message: 'should be string'
}];
return false;
}
var valid2 = errors === errs_2;
}
if (valid2) {
if (data1.smoothscroll === undefined) {
valid2 = true;
} else {
var errs_2 = errors;
if (typeof data1.smoothscroll !== "boolean") {
validate.errors = [{
keyword: 'type',
dataPath: (dataPath || '') + '.properties.smoothscroll',
schemaPath: '#/properties/properties/properties/smoothscroll/type',
params: {
type: 'boolean'
},
message: 'should be boolean'
}];
return false;
}
var valid2 = errors === errs_2;
}
if (valid2) {
if (data1.complete === undefined) {
valid2 = true;
} else {
var errs_2 = errors;
if (typeof data1.complete !== "string") {
validate.errors = [{
keyword: 'type',
dataPath: (dataPath || '') + '.properties.complete',
schemaPath: '#/properties/properties/properties/complete/type',
params: {
type: 'string'
},
message: 'should be string'
}];
return false;
}
var valid2 = errors === errs_2;
}
}
}
} else {
validate.errors = [{
keyword: 'type',
dataPath: (dataPath || '') + '.properties',
schemaPath: '#/properties/properties/type',
params: {
type: 'object'
},
message: 'should be object'
}];
return false;
}
var valid1 = errors === errs_1;
}
if (valid1) {
var data1 = data.blacklist;
if (data1 === undefined) {
valid1 = true;
} else {
var errs_1 = errors;
if (Array.isArray(data1)) {
var errs__1 = errors;
var valid1;
for (var i1 = 0; i1 < data1.length; i1++) {
var data2 = data1[i1];
var errs_2 = errors;
var errs__2 = errors;
var valid2 = false;
var errs_3 = errors;
if (typeof data2 !== "string") {
var err = {
keyword: 'type',
dataPath: (dataPath || '') + '.blacklist[' + i1 + ']',
schemaPath: '#/properties/blacklist/items/anyOf/0/type',
params: {
type: 'string'
},
message: 'should be string'
};
if (vErrors === null) vErrors = [err];
else vErrors.push(err);
errors++;
}
var valid3 = errors === errs_3;
valid2 = valid2 || valid3;
if (!valid2) {
var errs_3 = errors;
if ((data2 && typeof data2 === "object" && !Array.isArray(data2))) {
if (true) {
var errs__3 = errors;
var valid4 = true;
if (data2.url === undefined) {
valid4 = false;
var err = {
keyword: 'required',
dataPath: (dataPath || '') + '.blacklist[' + i1 + ']',
schemaPath: '#/properties/blacklist/items/anyOf/1/required',
params: {
missingProperty: 'url'
},
message: 'should have required property \'url\''
};
if (vErrors === null) vErrors = [err];
else vErrors.push(err);
errors++;
} else {
var errs_4 = errors;
if (typeof data2.url !== "string") {
var err = {
keyword: 'type',
dataPath: (dataPath || '') + '.blacklist[' + i1 + '].url',
schemaPath: '#/properties/blacklist/items/anyOf/1/properties/url/type',
params: {
type: 'string'
},
message: 'should be string'
};
if (vErrors === null) vErrors = [err];
else vErrors.push(err);
errors++;
}
var valid4 = errors === errs_4;
}
if (valid4) {
var data3 = data2.keys;
if (data3 === undefined) {
valid4 = false;
var err = {
keyword: 'required',
dataPath: (dataPath || '') + '.blacklist[' + i1 + ']',
schemaPath: '#/properties/blacklist/items/anyOf/1/required',
params: {
missingProperty: 'keys'
},
message: 'should have required property \'keys\''
};
if (vErrors === null) vErrors = [err];
else vErrors.push(err);
errors++;
} else {
var errs_4 = errors;
if (Array.isArray(data3)) {
var errs__4 = errors;
var valid4;
for (var i4 = 0; i4 < data3.length; i4++) {
var errs_5 = errors;
if (typeof data3[i4] !== "string") {
var err = {
keyword: 'type',
dataPath: (dataPath || '') + '.blacklist[' + i1 + '].keys[' + i4 + ']',
schemaPath: '#/properties/blacklist/items/anyOf/1/properties/keys/items/type',
params: {
type: 'string'
},
message: 'should be string'
};
if (vErrors === null) vErrors = [err];
else vErrors.push(err);
errors++;
}
var valid5 = errors === errs_5;
if (!valid5) break;
}
} else {
var err = {
keyword: 'type',
dataPath: (dataPath || '') + '.blacklist[' + i1 + '].keys',
schemaPath: '#/properties/blacklist/items/anyOf/1/properties/keys/type',
params: {
type: 'array'
},
message: 'should be array'
};
if (vErrors === null) vErrors = [err];
else vErrors.push(err);
errors++;
}
var valid4 = errors === errs_4;
}
}
}
} else {
var err = {
keyword: 'type',
dataPath: (dataPath || '') + '.blacklist[' + i1 + ']',
schemaPath: '#/properties/blacklist/items/anyOf/1/type',
params: {
type: 'object'
},
message: 'should be object'
};
if (vErrors === null) vErrors = [err];
else vErrors.push(err);
errors++;
}
var valid3 = errors === errs_3;
valid2 = valid2 || valid3;
}
if (!valid2) {
var err = {
keyword: 'anyOf',
dataPath: (dataPath || '') + '.blacklist[' + i1 + ']',
schemaPath: '#/properties/blacklist/items/anyOf',
params: {},
message: 'should match some schema in anyOf'
};
if (vErrors === null) vErrors = [err];
else vErrors.push(err);
errors++;
validate.errors = vErrors;
return false;
} else {
errors = errs__2;
if (vErrors !== null) {
if (errs__2) vErrors.length = errs__2;
else vErrors = null;
}
}
var valid2 = errors === errs_2;
if (!valid2) break;
}
} else {
validate.errors = [{
keyword: 'type',
dataPath: (dataPath || '') + '.blacklist',
schemaPath: '#/properties/blacklist/type',
params: {
type: 'array'
},
message: 'should be array'
}];
return false;
}
var valid1 = errors === errs_1;
}
}
}
}
}
} else {
validate.errors = [{
keyword: 'type',
dataPath: (dataPath || '') + "",
schemaPath: '#/type',
params: {
type: 'object'
},
message: 'should be object'
}];
return false;
}
validate.errors = vErrors;
return errors === 0;
};
})();
validate.schema = {
"type": "object",
"properties": {
"keymaps": {
"type": "object",
"patternProperties": {
".*": {
"type": "object",
"properties": {
"type": {
"type": "string"
}
},
"required": ["type"]
}
}
},
"search": {
"type": "object",
"properties": {
"default": {
"type": "string"
},
"engines": {
"type": "object",
"patternProperties": {
".*": {
"type": "string"
}
}
}
},
"required": ["default", "engines"]
},
"properties": {
"type": "object",
"properties": {
"hintchars": {
"type": "string"
},
"smoothscroll": {
"type": "boolean"
},
"complete": {
"type": "string"
}
}
},
"blacklist": {
"type": "array",
"items": {
"anyOf": [{
"type": "string"
}, {
"type": "object",
"properties": {
"url": {
"type": "string"
},
"keys": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": ["url", "keys"]
}]
}
}
},
"additionalProperties": false
};
validate.errors = null;
module.exports = validate;

@ -16,16 +16,6 @@ describe('BlacklistItem', () => {
expect(item.partial).to.be.true; expect(item.partial).to.be.true;
expect(item.keys).to.deep.equal(['j', 'k']); expect(item.keys).to.deep.equal(['j', 'k']);
}); });
it('throws a TypeError', () => {
expect(() => BlacklistItem.fromJSON(null)).to.throw(TypeError);
expect(() => BlacklistItem.fromJSON(100)).to.throw(TypeError);
expect(() => BlacklistItem.fromJSON({})).to.throw(TypeError);
expect(() => BlacklistItem.fromJSON({url: 'google.com'})).to.throw(TypeError);
expect(() => BlacklistItem.fromJSON({keys: ['a']})).to.throw(TypeError);
expect(() => BlacklistItem.fromJSON({url: 'google.com', keys: 10})).to.throw(TypeError);
expect(() => BlacklistItem.fromJSON({url: 'google.com', keys: ['a', 'b', 3]})).to.throw(TypeError);
});
}); });
describe('#matches', () => { describe('#matches', () => {
@ -118,14 +108,6 @@ describe('Blacklist', () => {
let blacklist = Blacklist.fromJSON([]); let blacklist = Blacklist.fromJSON([]);
expect(blacklist.toJSON()).to.deep.equals([]); expect(blacklist.toJSON()).to.deep.equals([]);
}); });
it('throws a TypeError', () => {
expect(() => Blacklist.fromJSON(null)).to.throw(TypeError);
expect(() => Blacklist.fromJSON(100)).to.throw(TypeError);
expect(() => Blacklist.fromJSON({})).to.throw(TypeError);
expect(() => Blacklist.fromJSON([100])).to.throw(TypeError);
expect(() => Blacklist.fromJSON([{}])).to.throw(TypeError);
})
}); });
describe('#includesEntireBlacklist', () => { describe('#includesEntireBlacklist', () => {

@ -19,7 +19,6 @@ describe('Keymaps', () => {
}); });
it('throws a TypeError by invalid settings', () => { it('throws a TypeError by invalid settings', () => {
expect(() => Keymaps.fromJSON(null)).to.throw(TypeError);
expect(() => Keymaps.fromJSON({ expect(() => Keymaps.fromJSON({
k: { type: "invalid.operation" }, k: { type: "invalid.operation" },
})).to.throw(TypeError); })).to.throw(TypeError);

@ -26,19 +26,6 @@ describe('Search', () => {
}); });
it('throws a TypeError by invalid settings', () => { it('throws a TypeError by invalid settings', () => {
expect(() => Search.fromJSON(null)).to.throw(TypeError);
expect(() => Search.fromJSON({})).to.throw(TypeError);
expect(() => Search.fromJSON([])).to.throw(TypeError);
expect(() => Search.fromJSON({
default: 123,
engines: {}
})).to.throw(TypeError);
expect(() => Search.fromJSON({
default: 'google',
engines: {
'google': 123456,
}
})).to.throw(TypeError);
expect(() => Search.fromJSON({ expect(() => Search.fromJSON({
default: 'wikipedia', default: 'wikipedia',
engines: { engines: {

@ -4,7 +4,7 @@
"module": "commonjs", "module": "commonjs",
"lib": ["es6", "dom", "es2017"], "lib": ["es6", "dom", "es2017"],
"allowJs": true, "allowJs": true,
"checkJs": true, "checkJs": false,
"jsx": "react", "jsx": "react",
"sourceMap": true, "sourceMap": true,
"outDir": "./build", "outDir": "./build",

@ -4,7 +4,7 @@ const path = require('path');
const src = path.resolve(__dirname, 'src'); const src = path.resolve(__dirname, 'src');
const dist = path.resolve(__dirname, 'build'); const dist = path.resolve(__dirname, 'build');
config = { const config = {
entry: { entry: {
content: path.join(src, 'content'), content: path.join(src, 'content'),
settings: path.join(src, 'settings'), settings: path.join(src, 'settings'),