ConfigManager

ConfigManager: Manage(Get, set and update) your config with config path simply with a ruleset!

สคริปต์นี้ไม่ควรถูกติดตั้งโดยตรง มันเป็นคลังสำหรับสคริปต์อื่น ๆ เพื่อบรรจุด้วยคำสั่งเมทา // @require https://update.greasyfork.org/scripts/449583/1334125/ConfigManager.js

/* eslint-disable no-multi-spaces */

// ==UserScript==
// @name               ConfigManager
// @namespace          ConfigManager
// @version            0.8.2
// @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.updateGlobal = updateGlobal;
	CM.getConfigVersion = getConfigVersion;
	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, base) {
				return isObject(config) ? new Proxy(config, {
					get: function(target, property, reciever) {
						const newPath = [...path, property];
						return makeProxy(inProto(target, property) ? target[property] : getConfig(newPath), [...path, property]);
					},
					set: function(target, property, value, reciever) {
						return (setConfig([...path, property], value), true);
					},
					deleteProperty: function(target, property) {
						const parent = getConfig(path);
						delete parent[property];
						setConfig(path, parent);
						return true;
					}
				}) : config;

				function inProto(obj, prop) {
					return prop in obj && !obj.hasOwnProperty(prop);
				}
			}
		},
		set: function(target, property, value, reciever) {
			return (_GM_setValue(property, value), true);
		},
		has: function(target, property) {
			return _GM_listValues().includes(property);
		},
		deleteProperty: function(target, property) {
			return (_GM_deleteValue(property), true);
		}
	});
	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);
		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.length && !(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 {
			const verKey = Ruleset['version-key'];
			const oldConfig = ConfigBase[target];
			if (isObject(value)) {
				hasProp(value, verKey) && (hasProp(oldConfig, verKey) ? value[verKey] !== oldConfig[verKey] : true) &&
					Err('Shouldn\'t manually set config version to a version number differs from current version number');
				value[verKey] = ConfigBase[target][verKey];
			}
			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'];
		let config = getConfig(basename);

		// Valid check
		if ([verKey, ...(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.call(CM, config);
			updated = true;
		}

		// Set version and save
		return save();

		function save() {
			isObject(config) && (config[verKey] = updaters.length);
			ConfigBase[basename] = config;
			return updated;
		}
	}

	function updateAllConfigs() {
		const keys = _GM_listValues();
		keys.forEach((key) => (updateConfig(key)));
	}

	function updateGlobal() {
		let updated = false;

		const updaters = Ruleset.globalUpdaters || [];
		const verKey = Ruleset['version-key'];
		if (!updaters.length) {
			return save();
		}
		const config = _GM_listValues().reduce((obj, key) => Object.assign(obj, { [key]: _GM_getValue(key) }), {});

		// Update
		for (let i = (config[verKey] || 0); i < updaters.length; i++) {
			const updater = updaters[i];
			config = updater.call(CM, config);
			updated = true;
		}

		// Set version and save
		return save();

		function save() {
			config[verKey] = updaters.length;
			Object.keys(config).forEach(key => _GM_setValue(key, config[key]));
			return updated;
		}
	}

	function getConfigVersion(basename=null) {
		const verKey = Ruleset['version-key'];
		return (basename ? ConfigBase[basename] : ConfigBase)[verKey] || 0;
	}

	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) {
				const val = getConfig([...path, key]);
				return typeof val === 'undefined' ? defaultValue : val;
			},
			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.replace(/^\//, '').replace(/\/$/, '').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);
	}
}