OptionsUtil refactor / options default values (#807)

* Replace _readFile with _fetchAsset for consistency with Backend

* Fix error messages

* Make OptionsUtil non-static

* Update how default options are assigned

* Add createValidatingProxy

* Add validate, update _onApiSetAllSettings

* Remove unused api.optionsSchemaGet

* Remove Backend._optionsSchema

* Update OptionsUtil to create its own JsonSchemaValidator

* Rename Backend._optionsSchemaValidator

* Make getDefault non-async
This commit is contained in:
toasted-nutbread 2020-09-11 14:15:08 -04:00 committed by GitHub
parent a1729eb9ae
commit f168efb69c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 77 additions and 49 deletions

View File

@ -41,8 +41,7 @@ class Backend {
this._mecab = new Mecab(); this._mecab = new Mecab();
this._clipboardMonitor = new ClipboardMonitor({getClipboard: this._onApiClipboardGet.bind(this)}); this._clipboardMonitor = new ClipboardMonitor({getClipboard: this._onApiClipboardGet.bind(this)});
this._options = null; this._options = null;
this._optionsSchema = null; this._profileConditionsSchemaValidator = new JsonSchemaValidator();
this._optionsSchemaValidator = new JsonSchemaValidator();
this._profileConditionsSchemaCache = []; this._profileConditionsSchemaCache = [];
this._profileConditionsUtil = new ProfileConditions(); this._profileConditionsUtil = new ProfileConditions();
this._defaultAnkiFieldTemplates = null; this._defaultAnkiFieldTemplates = null;
@ -55,6 +54,7 @@ class Backend {
requestBuilder: this._requestBuilder, requestBuilder: this._requestBuilder,
useCache: false useCache: false
}); });
this._optionsUtil = new OptionsUtil();
this._clipboardPasteTarget = null; this._clipboardPasteTarget = null;
this._clipboardPasteTargetInitialized = false; this._clipboardPasteTargetInitialized = false;
@ -78,7 +78,6 @@ class Backend {
this._messageHandlers = new Map([ this._messageHandlers = new Map([
['requestBackendReadySignal', {async: false, contentScript: true, handler: this._onApiRequestBackendReadySignal.bind(this)}], ['requestBackendReadySignal', {async: false, contentScript: true, handler: this._onApiRequestBackendReadySignal.bind(this)}],
['optionsSchemaGet', {async: false, contentScript: true, handler: this._onApiOptionsSchemaGet.bind(this)}],
['optionsGet', {async: false, contentScript: true, handler: this._onApiOptionsGet.bind(this)}], ['optionsGet', {async: false, contentScript: true, handler: this._onApiOptionsGet.bind(this)}],
['optionsGetFull', {async: false, contentScript: true, handler: this._onApiOptionsGetFull.bind(this)}], ['optionsGetFull', {async: false, contentScript: true, handler: this._onApiOptionsGetFull.bind(this)}],
['optionsSave', {async: true, contentScript: true, handler: this._onApiOptionsSave.bind(this)}], ['optionsSave', {async: true, contentScript: true, handler: this._onApiOptionsSave.bind(this)}],
@ -188,10 +187,9 @@ class Backend {
} }
await this._translator.prepare(); await this._translator.prepare();
this._optionsSchema = await this._fetchAsset('/bg/data/options-schema.json', true); await this._optionsUtil.prepare();
this._defaultAnkiFieldTemplates = (await this._fetchAsset('/bg/data/default-anki-field-templates.handlebars')).trim(); this._defaultAnkiFieldTemplates = (await this._fetchAsset('/bg/data/default-anki-field-templates.handlebars')).trim();
this._options = await OptionsUtil.load(); this._options = await this._optionsUtil.load();
this._options = this._optionsSchemaValidator.getValidValueOrDefault(this._optionsSchema, this._options);
this._applyOptions('background'); this._applyOptions('background');
@ -222,7 +220,7 @@ class Backend {
getFullOptions(useSchema=false) { getFullOptions(useSchema=false) {
const options = this._options; const options = this._options;
return useSchema ? this._optionsSchemaValidator.createProxy(options, this._optionsSchema) : options; return useSchema ? this._optionsUtil.createValidatingProxy(options) : options;
} }
getOptions(optionsContext, useSchema=false) { getOptions(optionsContext, useSchema=false) {
@ -370,10 +368,6 @@ class Backend {
} }
} }
_onApiOptionsSchemaGet() {
return this._optionsSchema;
}
_onApiOptionsGet({optionsContext}) { _onApiOptionsGet({optionsContext}) {
return this.getOptions(optionsContext); return this.getOptions(optionsContext);
} }
@ -385,7 +379,7 @@ class Backend {
async _onApiOptionsSave({source}) { async _onApiOptionsSave({source}) {
this._clearProfileConditionsSchemaCache(); this._clearProfileConditionsSchemaCache();
const options = this.getFullOptions(); const options = this.getFullOptions();
await OptionsUtil.save(options); await this._optionsUtil.save(options);
this._applyOptions(source); this._applyOptions(source);
} }
@ -787,7 +781,8 @@ class Backend {
} }
async _onApiSetAllSettings({value, source}) { async _onApiSetAllSettings({value, source}) {
this._options = this._optionsSchemaValidator.getValidValueOrDefault(this._optionsSchema, value); this._optionsUtil.validate(value);
this._options = clone(value);
await this._onApiOptionsSave({source}); await this._onApiOptionsSave({source});
} }
@ -1014,7 +1009,7 @@ class Backend {
this._profileConditionsSchemaCache.push(schema); this._profileConditionsSchemaCache.push(schema);
} }
if (conditionGroups.length > 0 && this._optionsSchemaValidator.isValid(optionsContext, schema)) { if (conditionGroups.length > 0 && this._profileConditionsSchemaValidator.isValid(optionsContext, schema)) {
return profile; return profile;
} }
++index; ++index;
@ -1025,7 +1020,7 @@ class Backend {
_clearProfileConditionsSchemaCache() { _clearProfileConditionsSchemaCache() {
this._profileConditionsSchemaCache = []; this._profileConditionsSchemaCache = [];
this._optionsSchemaValidator.clearCache(); this._profileConditionsSchemaValidator.clearCache();
} }
_checkLastError() { _checkLastError() {

View File

@ -149,7 +149,7 @@ class JsonSchemaValidator {
getValidValueOrDefault(schema, value) { getValidValueOrDefault(schema, value) {
let type = this._getValueType(value); let type = this._getValueType(value);
const schemaType = schema.type; const schemaType = schema.type;
if (!this._isValueTypeAny(value, type, schemaType)) { if (typeof value === 'undefined' || !this._isValueTypeAny(value, type, schemaType)) {
let assignDefault = true; let assignDefault = true;
const schemaDefault = schema.default; const schemaDefault = schema.default;

View File

@ -15,8 +15,21 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
/* global
* JsonSchemaValidator
*/
class OptionsUtil { class OptionsUtil {
static async update(options) { constructor() {
this._schemaValidator = new JsonSchemaValidator();
this._optionsSchema = null;
}
async prepare() {
this._optionsSchema = await this._fetchAsset('/bg/data/options-schema.json', true);
}
async update(options) {
// Invalid options // Invalid options
if (!isObject(options)) { if (!isObject(options)) {
options = {}; options = {};
@ -72,14 +85,14 @@ class OptionsUtil {
return await this._applyUpdates(options, this._getVersionUpdates()); return await this._applyUpdates(options, this._getVersionUpdates());
} }
static async load() { async load() {
let options = null; let options;
try { try {
const optionsStr = await new Promise((resolve, reject) => { const optionsStr = await new Promise((resolve, reject) => {
chrome.storage.local.get(['options'], (store) => { chrome.storage.local.get(['options'], (store) => {
const error = chrome.runtime.lastError; const error = chrome.runtime.lastError;
if (error) { if (error) {
reject(new Error(error)); reject(new Error(error.message));
} else { } else {
resolve(store.options); resolve(store.options);
} }
@ -90,15 +103,21 @@ class OptionsUtil {
// NOP // NOP
} }
return await this.update(options); if (typeof options !== 'undefined') {
options = await this.update(options);
}
options = this._schemaValidator.getValidValueOrDefault(this._optionsSchema, options);
return options;
} }
static save(options) { save(options) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
chrome.storage.local.set({options: JSON.stringify(options)}, () => { chrome.storage.local.set({options: JSON.stringify(options)}, () => {
const error = chrome.runtime.lastError; const error = chrome.runtime.lastError;
if (error) { if (error) {
reject(new Error(error)); reject(new Error(error.message));
} else { } else {
resolve(); resolve();
} }
@ -106,13 +125,21 @@ class OptionsUtil {
}); });
} }
static async getDefault() { getDefault() {
return await this.update({}); return this._schemaValidator.getValidValueOrDefault(this._optionsSchema);
}
createValidatingProxy(options) {
return this._schemaValidator.createProxy(options, this._optionsSchema);
}
validate(options) {
return this._schemaValidator.validate(options, this._optionsSchema);
} }
// Legacy profile updating // Legacy profile updating
static _legacyProfileUpdateGetUpdates() { _legacyProfileUpdateGetUpdates() {
return [ return [
null, null,
null, null,
@ -203,7 +230,7 @@ class OptionsUtil {
]; ];
} }
static _legacyProfileUpdateGetDefaults() { _legacyProfileUpdateGetDefaults() {
return { return {
general: { general: {
enable: true, enable: true,
@ -302,7 +329,7 @@ class OptionsUtil {
}; };
} }
static _legacyProfileUpdateAssignDefaults(options) { _legacyProfileUpdateAssignDefaults(options) {
const defaults = this._legacyProfileUpdateGetDefaults(); const defaults = this._legacyProfileUpdateGetDefaults();
const combine = (target, source) => { const combine = (target, source) => {
@ -323,7 +350,7 @@ class OptionsUtil {
return options; return options;
} }
static _legacyProfileUpdateUpdateVersion(options) { _legacyProfileUpdateUpdateVersion(options) {
const updates = this._legacyProfileUpdateGetUpdates(); const updates = this._legacyProfileUpdateGetUpdates();
this._legacyProfileUpdateAssignDefaults(options); this._legacyProfileUpdateAssignDefaults(options);
@ -345,20 +372,20 @@ class OptionsUtil {
// Private // Private
static async _addFieldTemplatesToOptions(options, additionSourceUrl) { async _addFieldTemplatesToOptions(options, additionSourceUrl) {
let addition = null; let addition = null;
for (const {options: profileOptions} of options.profiles) { for (const {options: profileOptions} of options.profiles) {
const fieldTemplates = profileOptions.anki.fieldTemplates; const fieldTemplates = profileOptions.anki.fieldTemplates;
if (fieldTemplates !== null) { if (fieldTemplates !== null) {
if (addition === null) { if (addition === null) {
addition = await this._readFile(additionSourceUrl); addition = await this._fetchAsset(additionSourceUrl);
} }
profileOptions.anki.fieldTemplates = this._addFieldTemplatesBeforeEnd(fieldTemplates, addition); profileOptions.anki.fieldTemplates = this._addFieldTemplatesBeforeEnd(fieldTemplates, addition);
} }
} }
} }
static async _addFieldTemplatesBeforeEnd(fieldTemplates, addition) { async _addFieldTemplatesBeforeEnd(fieldTemplates, addition) {
const pattern = /[ \t]*\{\{~?>\s*\(\s*lookup\s*\.\s*"marker"\s*\)\s*~?\}\}/; const pattern = /[ \t]*\{\{~?>\s*\(\s*lookup\s*\.\s*"marker"\s*\)\s*~?\}\}/;
const newline = '\n'; const newline = '\n';
let replaced = false; let replaced = false;
@ -373,7 +400,7 @@ class OptionsUtil {
return fieldTemplates; return fieldTemplates;
} }
static async _readFile(url) { async _fetchAsset(url, json=false) {
url = chrome.runtime.getURL(url); url = chrome.runtime.getURL(url);
const response = await fetch(url, { const response = await fetch(url, {
method: 'GET', method: 'GET',
@ -383,10 +410,13 @@ class OptionsUtil {
redirect: 'follow', redirect: 'follow',
referrerPolicy: 'no-referrer' referrerPolicy: 'no-referrer'
}); });
return await response.text(); if (!response.ok) {
throw new Error(`Failed to fetch ${url}: ${response.status}`);
}
return await (json ? response.json() : response.text());
} }
static _getStringHashCode(string) { _getStringHashCode(string) {
let hashCode = 0; let hashCode = 0;
if (typeof string !== 'string') { return hashCode; } if (typeof string !== 'string') { return hashCode; }
@ -399,7 +429,7 @@ class OptionsUtil {
return hashCode; return hashCode;
} }
static async _applyUpdates(options, updates) { async _applyUpdates(options, updates) {
const targetVersion = updates.length; const targetVersion = updates.length;
let currentVersion = options.version; let currentVersion = options.version;
@ -417,7 +447,7 @@ class OptionsUtil {
return options; return options;
} }
static _getVersionUpdates() { _getVersionUpdates() {
return [ return [
{ {
async: false, async: false,
@ -438,7 +468,7 @@ class OptionsUtil {
]; ];
} }
static _updateVersion1(options) { _updateVersion1(options) {
// Version 1 changes: // Version 1 changes:
// Added options.global.database.prefixWildcardsSupported = false. // Added options.global.database.prefixWildcardsSupported = false.
options.global = { options.global = {
@ -449,7 +479,7 @@ class OptionsUtil {
return options; return options;
} }
static _updateVersion2(options) { _updateVersion2(options) {
// Version 2 changes: // Version 2 changes:
// Legacy profile update process moved into this upgrade function. // Legacy profile update process moved into this upgrade function.
for (const profile of options.profiles) { for (const profile of options.profiles) {
@ -461,14 +491,14 @@ class OptionsUtil {
return options; return options;
} }
static async _updateVersion3(options) { async _updateVersion3(options) {
// Version 3 changes: // Version 3 changes:
// Pitch accent Anki field templates added. // Pitch accent Anki field templates added.
await this._addFieldTemplatesToOptions(options, '/bg/data/anki-field-templates-upgrade-v2.handlebars'); await this._addFieldTemplatesToOptions(options, '/bg/data/anki-field-templates-upgrade-v2.handlebars');
return options; return options;
} }
static async _updateVersion4(options) { async _updateVersion4(options) {
// Version 4 changes: // Version 4 changes:
// Options conditions converted to string representations. // Options conditions converted to string representations.
// Added usePopupWindow. // Added usePopupWindow.

View File

@ -26,9 +26,12 @@ class BackupController {
this._settingsExportToken = null; this._settingsExportToken = null;
this._settingsExportRevoke = null; this._settingsExportRevoke = null;
this._currentVersion = 0; this._currentVersion = 0;
this._optionsUtil = new OptionsUtil();
} }
prepare() { async prepare() {
await this._optionsUtil.prepare();
document.querySelector('#settings-export').addEventListener('click', this._onSettingsExportClick.bind(this), false); document.querySelector('#settings-export').addEventListener('click', this._onSettingsExportClick.bind(this), false);
document.querySelector('#settings-import').addEventListener('click', this._onSettingsImportClick.bind(this), false); document.querySelector('#settings-import').addEventListener('click', this._onSettingsImportClick.bind(this), false);
document.querySelector('#settings-import-file').addEventListener('change', this._onSettingsImportFileChange.bind(this), false); document.querySelector('#settings-import-file').addEventListener('change', this._onSettingsImportFileChange.bind(this), false);
@ -140,7 +143,11 @@ class BackupController {
// Importing // Importing
async _settingsImportSetOptionsFull(optionsFull) { async _settingsImportSetOptionsFull(optionsFull) {
await this._settingsController.setAllSettings(optionsFull); try {
await this._settingsController.setAllSettings(optionsFull);
} catch (e) {
yomichan.logError(e);
}
} }
_showSettingsImportError(error) { _showSettingsImportError(error) {
@ -322,7 +329,7 @@ class BackupController {
} }
// Upgrade options // Upgrade options
optionsFull = await OptionsUtil.update(optionsFull); optionsFull = await this._optionsUtil.update(optionsFull);
// Check for warnings // Check for warnings
const sanitizationWarnings = this._settingsImportSanitizeOptions(optionsFull, true); const sanitizationWarnings = this._settingsImportSanitizeOptions(optionsFull, true);
@ -368,7 +375,7 @@ class BackupController {
$('#settings-reset-modal').modal('hide'); $('#settings-reset-modal').modal('hide');
// Get default options // Get default options
const optionsFull = await OptionsUtil.getDefault(); const optionsFull = this._optionsUtil.getDefault();
// Assign options // Assign options
await this._settingsImportSetOptionsFull(optionsFull); await this._settingsImportSetOptionsFull(optionsFull);

View File

@ -49,10 +49,6 @@ const api = (() => {
// Invoke functions // Invoke functions
optionsSchemaGet() {
return this._invoke('optionsSchemaGet');
}
optionsGet(optionsContext) { optionsGet(optionsContext) {
return this._invoke('optionsGet', {optionsContext}); return this._invoke('optionsGet', {optionsContext});
} }