Skip to content

Instantly share code, notes, and snippets.

@hlfbt
Last active February 5, 2018 13:57
Show Gist options
  • Save hlfbt/2f6b6552046c493c76ae74f4d0e4cd6d to your computer and use it in GitHub Desktop.
Save hlfbt/2f6b6552046c493c76ae74f4d0e4cd6d to your computer and use it in GitHub Desktop.
Generic object class construction
/**
* GenericObject / objectify.js
*
* Creating new objects with neat setters and getters over again is annoying, so let's automate it.
*
* @author Alexander Schulz (alex@nope.bz)
*/
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
};
})();
/**
* 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\"}" */ });
@hlfbt
Copy link
Author

hlfbt commented Jan 29, 2018

TODOs:

  • add "strict" and "non-strict" type checking, autoconvert values on non-strict => maybe just use setters/getters...?
  • custom getter/setter per property
  • custom validator per property
  • nicer error message, maybe allow for descriptions of properties
  • documentation?!?!?!??!?!?!?!??!?!?!?!??!
    • surely examples are documentation enough

ETA: whenever I feel like it. Probably never.

shit's tite yo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment