ts-deserializable
Version:
Decorator pattern for deserializing unverified data to an instance of a class in typescript.
106 lines (105 loc) • 5.57 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const ds_prop_decorator_1 = require("./ds-prop.decorator");
const ds_reporter_1 = require("./ds-reporter");
const ds_attributes_1 = require("./ds-attributes");
const ds_operations_1 = require("./ds-operations");
class DsClassBuilder {
constructor() {
this.ignore = () => this.decoratorFactory(ds_reporter_1.ReportType.Ignore);
this.warn = () => this.decoratorFactory(ds_reporter_1.ReportType.Warn);
this.error = () => this.decoratorFactory(ds_reporter_1.ReportType.Error);
this.throw = (errorCtor = Error) => this.decoratorFactory(ds_reporter_1.ReportType.Throw, errorCtor);
this.getFallback = (fallback) => ((typeof fallback === 'function')
? fallback()
: fallback);
this.decoratorFactory = (reportType, errorCtor = Error) => ((target) => {
// save a reference to the original constructor
const originalConstructor = target;
// a utility function to generate instances of a class
function instanciate(constructor, ...args) {
let instance = new constructor(...args);
return instance;
}
// the new constructor behavior
const newConstructor = function (...args) {
return instanciate(originalConstructor, ...args);
};
// deserialization function
const deserializeFactory = (userDeserializer) => ((obj) => {
let attributes = ds_attributes_1.DsAttributes.get(target.prototype);
let operations = ds_operations_1.DsOperations.get(target.prototype);
let ret = newConstructor();
let reporter = new ds_reporter_1.DeserializerReporter(reportType, originalConstructor.name, errorCtor);
// map values and apply operations
Object.keys(attributes).forEach((key) => {
let attribute = attributes[key];
let value = (obj[key] !== undefined)
? obj[key]
: undefined;
// the breaker will stop the evaluation chain and
// instead force us to return the fallback value.
// TODO: how this is handled can be improved for better logging and debugging
let breaker = false;
let invalid = false;
let invalidWith;
// apply operations
if (operations[key]) {
value = operations[key].reduce((val, operation) => {
if (breaker) {
return val;
}
switch (operation.type) {
case ds_prop_decorator_1.DsOperationType.Validator:
if (!operation.func(val)) {
breaker = true;
invalid = true;
invalidWith = val;
}
return val;
case ds_prop_decorator_1.DsOperationType.Resolver:
let res = operation.func(obj);
breaker = (res === undefined);
return res;
case ds_prop_decorator_1.DsOperationType.Operator:
default:
let opRes = operation.func(val);
breaker = (opRes === undefined);
return opRes;
}
}, value);
}
// if breaker was tripped log invalid
if (breaker && invalid) {
value = this.getFallback(attribute.fallback);
reporter.markInvalid(key, invalidWith, value);
}
// mark if breaker was tripped or value does not exist
else if (breaker || value === undefined) {
value = this.getFallback(attribute.fallback);
reporter.mark(key, undefined, value);
}
ret[key] = value;
});
// report issues
reporter.report();
// call user deserializer function if it exists
if (userDeserializer && typeof userDeserializer === 'function') {
userDeserializer(obj, ret);
}
return ret;
});
// copy prototype so instanceof operator still works
newConstructor.prototype = originalConstructor.prototype;
// set a deserializer that also calls the user declared function
newConstructor.prototype.deserialize = deserializeFactory(newConstructor.prototype.deserialize);
// return new constructor (will override original)
return newConstructor;
});
}
}
/**
* Exported as a builder class instance purely for consistency sake.
* Might be useful in the future for added functionality
*/
exports.DsClass = () => new DsClassBuilder();