Last active
February 5, 2018 13:57
-
-
Save hlfbt/2f6b6552046c493c76ae74f4d0e4cd6d to your computer and use it in GitHub Desktop.
Generic object class construction
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* GenericObject / objectify.js | |
* | |
* Creating new objects with neat setters and getters over again is annoying, so let's automate it. | |
* | |
* @author Alexander Schulz ([email protected]) | |
*/ | |
var Objectify = (function () { | |
"use strict"; | |
var objectify = function objectify(config) { | |
if (typeof config !== "object") { | |
throw new SyntaxError("Missing object config"); | |
} | |
if (!("_this" in config && "name" in config._this)) { | |
throw new SyntaxError("An object name is required"); | |
} | |
var name = config._this.name; | |
var genericBody = "{\n" + | |
" var constructor = Object.getPrototypeOf(this);\n" + | |
" Object.defineProperties(this, {\n" + | |
" name: { value: name },\n" + | |
" displayName: { value: name },\n" + | |
" __props__: { value: config }\n" + | |
" });\n" + | |
"\n" + | |
" return constructor.call(this, staticStorage, obj);\n" + | |
"}"; | |
// staticStorage needs to be declared and ""bound"" in this context to actually be static in between instances | |
// The Function constructor is used to set the name, setting the name and displayName property may not work everywhere sadly | |
var generic = (new Function("staticStorage", "name", "config", "return function " + name + "(obj) " + genericBody))({}, name, config); | |
// If the length doesn't match then that means someone tried to inject a different function through the name variable! | |
if (generic.toString().length !== 15 + name.length + genericBody.length) { | |
throw new Error("Constructor didn't match expected length"); | |
} | |
Object.defineProperty(generic, "prototype", { | |
value: GenericObject, | |
writable: false, | |
configurable: false, | |
enumerable: false | |
}); | |
return generic; | |
}; | |
var GenericObject = function GenericObject(staticStorage, obj) { | |
if (typeof this.constructor === "undefined" || (this.constructor.name || "window").toLowerCase() === "window" || this.constructor === Object) { | |
throw new TypeError("Must be called with 'new'"); | |
} | |
var self = this; | |
// Contains all the meta information about this object | |
var _properties = this.__props__ || { _this: {} }; | |
// Actual value storage for all "normal" values | |
var _privateStorage = {}; | |
// Value storage for all static values shared between instances | |
var _staticStorage = staticStorage || {}; | |
// Pseudo-object with getters and setters for easier accessing | |
var _values = {}; | |
var _valid = typeof obj === _properties._this.type; | |
var _invalidAt = null; | |
var isType = function (val, type) { | |
return typeof type === "string" ? typeof val === type : val instanceof type; | |
}; | |
var typeName = function (type) { | |
return typeof type === "string" ? type : (type.name || ("" + type)); | |
}; | |
if ("name" in _properties._this) { | |
Object.defineProperties(this, { | |
name: { value: _properties._this.name }, | |
displayName: { value: _properties._this.name } | |
}); | |
} | |
// Copy over default property configurations to each property definition | |
for (var prop in _properties) { | |
if (prop !== "_this") { | |
for (var conf in _properties._this.defaults) { | |
if (!(conf in _properties[prop])) _properties[prop][conf] = _properties._this.defaults[conf]; | |
} | |
} | |
} | |
if (_valid) { | |
// Validating the passed object | |
for (var prop in _properties) { | |
// Skip the special _this property | |
if (prop === "_this") continue; | |
// Is the property present? | |
if (prop in obj) { | |
// Does the type match? | |
if (isType(obj[prop], _properties[prop].type)) continue; | |
// Is the property required? | |
} else if (!_properties[prop].required) continue; | |
_valid = false; | |
_invalidAt = prop; | |
break; | |
} | |
} | |
if (!_valid) { | |
var expected = { | |
str: typeName(_properties._this.type) + " {", | |
idx: {} // Can be used later to give a little arrow pointing to it instead of the > | |
}; | |
for (var prop in _properties) { | |
if (prop !== "_this") { | |
if (!_properties[prop].required) expected.str += "["; | |
if (_invalidAt === prop) expected.str += ">"; | |
expected.idx[prop] = expected.str.length - 1; | |
expected.str += prop + ":" + typeName(_properties[prop].type); | |
if (!_properties[prop].required) expected.str += "]"; | |
expected.str += ", "; | |
} | |
} | |
expected.str = expected.str.substring(0, expected.str.length - 2) + "}"; | |
throw new TypeError("Invalid argument passed, expecting: " + expected.str); | |
} | |
for (var prop in _properties) { | |
if (prop !== "_this") { | |
// The camel-cased property name is ok to be saved in the static _properties object since it's imperatively dependend on the property's name anyway | |
_properties[prop].camelized = prop.replace(/[\s_-]{2,}/g, function ($0) { return $0[0]; }).replace(/(?:^\s*|\s+|[_-])(.)/g, function ($0, $1) { return $1.toUpperCase(); }); | |
// This is mainly done for making extending this later on easier. | |
// If this may ever be extended to have, for example, protected properties as well, | |
// or more functionality that accesses the value storages is added, then the _values object | |
// will be much nicer to work with then having to add if/else cases everywhere. | |
if (_properties[prop].static === true) { | |
_staticStorage[prop] = obj[prop] || undefined; | |
Object.defineProperty(_values, prop, { | |
get: (function (prop) { return _staticStorage[prop]; }).bind(this, prop), | |
set: (function (prop, value) { return _staticStorage[prop] = value; }).bind(this, prop) | |
}); | |
} else { | |
_privateStorage[prop] = obj[prop] || undefined; | |
Object.defineProperty(_values, prop, { | |
get: (function (prop) { return _privateStorage[prop]; }).bind(this, prop), | |
set: (function (prop, value) { return _privateStorage[prop] = value; }).bind(this, prop) | |
}); | |
} | |
var getter = (function (prop) { | |
return _values[prop]; | |
}).bind(this, prop); | |
var setter = (function (prop, val) { | |
if (_properties[prop].strictSetter && !isType(val, _properties[prop].type)) { | |
throw new TypeError(prop + " must be of type " + typeName(_properties[prop].type)); | |
} | |
_values[prop] = val; | |
// This totally works and it's awesomely convenient | |
// (returns the assigned value with the native setters but the object with the setter methods) | |
return self; | |
}).bind(this, prop); | |
if (_properties[prop].public) { | |
Object.defineProperty(this, prop, { | |
get: getter, | |
set: setter, | |
configurable: false, | |
enumerable: true | |
}); | |
} | |
if (_properties._this.getters) { | |
Object.defineProperty(this, "get" + _properties[prop].camelized, { | |
value: getter, | |
writable: false, | |
configurable: false, | |
enumerable: false | |
}); | |
} | |
if (_properties._this.setters) { | |
Object.defineProperty(this, "set" + _properties[prop].camelized, { | |
value: setter, | |
writable: false, | |
configurable: false, | |
enumerable: false | |
}); | |
} | |
} | |
} | |
Object.defineProperty(this, "isValid", { | |
value: function () { return _valid; }, | |
writable: false, | |
configurable: false, | |
enumerable: false | |
}); | |
}; | |
return { | |
objectify: objectify, | |
GenericObject: GenericObject | |
}; | |
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* These are some simple usage examples of objectify.js. | |
* Just copy-paste objectify.js and these examples into the JS console of your choice and see for yourself! | |
*/ | |
var dummyConfig = { | |
_this: { | |
name: "DummyObject", | |
type: "object", | |
getters: true, | |
setters: true, | |
defaults: { | |
strictSetter: true, | |
public: false | |
} | |
}, | |
data: { | |
required: true, | |
type: "string", | |
}, | |
staticData: { | |
required: false, | |
type: "string", | |
strictSetter: false, | |
static: true | |
}, | |
publicData: { | |
required: false, | |
type: "string", | |
public: true | |
}, | |
time: { | |
required: false, | |
type: Date | |
} | |
}; | |
var DummyObject = Objectify.objectify(dummyConfig); | |
// Catch any thrown error and output it to console.error | |
function test(fun) { | |
try { | |
console.log("> " + fun.toString().replace(/^\s*(?:function\s*\(\s*\)|\(\s*\)\s*=\s*>)\s*\{\s*(?:return)?/, '').replace(/}$/, '').replace(/^\s*|\s*$/g, '')); | |
let ret = fun.call(); | |
console.log(ret instanceof Object ? ret : JSON.stringify(ret)); | |
} catch (e) { | |
console.error("ERROR:", e); | |
} | |
} | |
test(()=>{ new DummyObject(); }); | |
test(()=>{ DummyObject({data: "test data", time: new Date()}); }); | |
test(()=>{ new DummyObject({ faultyData: "uh-oh" }); }); | |
console.log(""); | |
console.log(""); | |
var d1 = new DummyObject({ | |
data: "Hello World!", | |
time: new Date() | |
}); | |
var d2 = new DummyObject({ | |
data: "This object has no time" | |
}); | |
test(()=>{ return d1.data; /* => undefined */ }); | |
test(()=>{ return d1.getData(); /* => "Hello World!" */ }); | |
test(()=>{ return d2.getData(); /* => "This object has no time" */ }); | |
console.log(""); | |
test(()=>{ return d2.getTime(); /* => undefined */ }); | |
test(()=>{ return d2.setTime(1337); /* => ERROR: TypeError: time must be of type Date */ }); | |
test(()=>{ return d2.setTime(new Date()); /* => DummyObject { ... */ }); | |
test(()=>{ return d2.getTime(); /* => Wed Jan 31 2018 ... */ }); | |
console.log(""); | |
test(()=>{ return d1.getStaticData(); /* => undefined */ }); | |
test(()=>{ return d2.setStaticData(3.14); /* => DummyObject { ... */ }); | |
test(()=>{ return d1.getStaticData(); /* => 3.14 */ }); | |
console.log(""); | |
test(()=>{ return d1.publicData = "this is an enumerable property"; /* => "this is an enumerable property" */ }); | |
test(()=>{ for (var p in d1) { console.log(p, ":", d1[p]); }; /* => publicData : this is ... */ }); | |
test(()=>{ return JSON.stringify(d1); /* => "{\"publicData\":\"this is an enumerable property\"}" */ }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
TODOs:
ETA: whenever I feel like it. Probably never.
shit's tite yo