/* eslint-disable no-multi-spaces */
// ==UserScript==
// @name ConfigManager
// @namespace ConfigManager
// @version 0.6.6
// @description ConfigManager: Manage(Get, set and update) your config with config path simply with a ruleset!
// @author PY-DNG
// @license GPL-v3
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_listValues
// @grant GM_deleteValue
// ==/UserScript==
function ConfigManager(Ruleset, storage={}) {
const CM = this;
const _GM_setValue = storage.GM_setValue || GM_setValue || Err('ConfigManager: could not find GM_setValue');
const _GM_getValue = storage.GM_getValue || GM_getValue || Err('ConfigManager: could not find GM_getValue');
const _GM_listValues = storage.GM_listValues || GM_listValues || Err('ConfigManager: could not find GM_listValues');
const _GM_deleteValue = storage.GM_deleteValue || GM_deleteValue || Err('ConfigManager: could not find GM_deleteValue');
const ConfigBase = new Proxy({}, {
get: function(target, property, reciever) {
return _GM_getValue(property);
},
set: function(target, property, value, reciever) {
return (_GM_setValue(property, value), true);
},
has: function(target, property) {
return _GM_listValues().includes(property);
}
});
CM.getConfig = getConfig;
CM.setConfig = setConfig;
CM.updateConfig = updateConfig;
CM.updateAllConfigs = updateAllConfigs;
CM.setDefaults = setDefaults;
CM.readPath = readPath;
CM.pathExists = pathExists;
CM.mergePath = mergePath;
CM.getBaseName = getBaseName;
CM.makeSubStorage = makeSubStorage;
CM.Config = new Proxy({}, {
get: function(target, property, reciever) {
return makeProxy(getConfig(property), [property]);
function makeProxy(config, path) {
return isObject(config) ? new Proxy(config, {
get: function(target, property, reciever) {
return makeProxy(target[property], [...path, property]);
},
set: function(target, property, value, reciever) {
return (setConfig([...path, property], value), true);
}
}) : config;
}
},
set: function(target, property, value, reciever) {
return (_GM_setValue(property, value), true);
},
has: function(target, property) {
return _GM_listValues().includes(property);
}
});
Object.freeze(CM);
// Get config value from path (e.g. 'Users/username/' or ['Users', 12345])
function getConfig(path) {
// Split path
path = arrPath(path);
// Init config if need
if (!(path[0] in ConfigBase)) {
ConfigBase[path[0]] = Ruleset.defaultValues[path[0]];
}
// Get config by path
const target = path.pop();
const config = readPath(ConfigBase, path);
!hasProp(config, target) && Err('path not exist');;
return config[target];
}
// Set config value to path
function setConfig(path, value) {
path = arrPath(path);
const target = path.pop();
// Init config if need
if (!(path[0] in ConfigBase)) {
ConfigBase[path[0]] = Ruleset.defaultValues[path[0]];
}
if (path.length > 0) {
const basekey = path.shift();
const baseobj = ConfigBase[basekey];
let config = readPath(baseobj, path);
if (isObject(config)) {
config[target] = value;
ConfigBase[basekey] = baseobj;
} else {
Err('Attempt to set a property to a non-object value')
}
} else {
ConfigBase[target] = value;
}
}
function updateConfig(basename) {
let updated = false;
// Get updaters and config
const updaters = Ruleset.updaters.hasOwnProperty(basename) ? Ruleset.updaters[basename] : [];
const verKey = Ruleset['version-key'];
const config = getConfig(basename);
// Valid check
if (Ruleset.ignores.includes(basename)) {
return false;
}
if (!updaters.length) {
return save();
}
// Update
for (let i = (config[verKey] || 0); i < updaters.length; i++) {
const updater = updaters[i];
config = updater(config);
updated = true;
}
// Set version and save
return save();
function save() {
config[verKey] = updaters.length;
setConfig(basename, config);
return updated;
}
}
function updateAllConfigs() {
const keys = _GM_listValues();
keys.forEach((key) => (updateConfig(key)));
}
function setDefaults() {
for (const [key, val] of Object.entries(Ruleset.defaultValues)) {
!(key in ConfigBase) && (ConfigBase[key] = val);
}
}
function makeSubStorage(path) {
path = arrPath(path);
return {
GM_setValue: function(key, value) {
setConfig([...path, key], value);
},
GM_getValue: function(key, defaultValue) {
try {
return getConfig([...path, key]);
} catch (err) {
return defaultValue;
}
},
GM_listValues: function() {
return Object.keys(getConfig(path));
},
GM_deleteValue: function(key) {
const parent = getConfig(path);
delete parent[key];
setConfig(path, parent);
}
}
}
function readPath(obj, path) {
path = arrPath(path);
while (path.length > 0) {
const key = path.shift();
if (isObject(obj) && hasProp(obj, key)) {
obj = obj[key];
} else {
Err('Attempt to read a property that is not exist (reading "' + key + '" in path "' + path + '")');
}
}
return obj;
}
function pathExists(obj, path) {
path = arrPath(path);
while (path.length > 0) {
const key = path.shift();
if (isObject(obj) && hasProp(obj, key)) {
obj = obj[key];
} else {
return false;
}
}
return true;
}
function mergePath() {
return Array.from(arguments).join('/');
}
function getBaseName(path) {
return arrPath(path)[0];
}
function getPathWithoutBase(path) {
const p = arrPath(path);
p.shift();
return p;
}
function arrPath(strpath) {
return Array.isArray(strpath) ? [...strpath] : strpath.split('/');
}
function isObject(obj) {
return typeof obj === 'object' && obj !== null;
}
function hasProp(obj, prop) {
return obj === ConfigBase ? prop in obj : obj.hasOwnProperty(prop);
}
// type: [Error, TypeError]
function Err(msg, type=0) {
throw new [Error, TypeError][type](msg);
}
}