Add guarded array support
parent
d77b042299
commit
8ac9a02d2d
@ -0,0 +1,17 @@
|
||||
"use strict";
|
||||
|
||||
const dm = require("../");
|
||||
|
||||
let Thing = dm.createType("Thing", {
|
||||
value: dm.string()
|
||||
});
|
||||
|
||||
let item = Thing({
|
||||
value: "foo"
|
||||
});
|
||||
|
||||
console.log(item);
|
||||
|
||||
delete item.value;
|
||||
|
||||
console.log(item);
|
@ -0,0 +1,136 @@
|
||||
"use strict";
|
||||
|
||||
const errors = require("../errors");
|
||||
const getValueType = require("../util/get-value-type");
|
||||
|
||||
let indexRegex = /^[0-9]+$/;
|
||||
|
||||
module.exports = function createGuardedArray(array, guard, _, parent) {
|
||||
function generateArraySignatureError() {
|
||||
let wantedSignature = {
|
||||
_guardedCollectionType: "array",
|
||||
_itemType: guard._rule
|
||||
};
|
||||
|
||||
return new errors.ValidationError(`Expected an array or ${getValueType(wantedSignature)}, got ${getValueType(array)} instead`);
|
||||
}
|
||||
|
||||
if (array._guardedCollectionType === "array") {
|
||||
if (guard === array._itemType) {
|
||||
return array;
|
||||
} else {
|
||||
throw generateArraySignatureError();
|
||||
}
|
||||
} else if (Array.isArray(array)) {
|
||||
for (let value of array) {
|
||||
check(value);
|
||||
}
|
||||
|
||||
/* We shallow-clone the array here, to keep users from accidentally mutating the source array outside the purview of the guards specified here. */
|
||||
let targetArray = array.slice();
|
||||
|
||||
function check(value) {
|
||||
let guardResult = guard.call(parent, value);
|
||||
|
||||
if (guardResult === true) {
|
||||
return true;
|
||||
} else {
|
||||
throw guardResult;
|
||||
}
|
||||
}
|
||||
|
||||
let guardedForEach = guardedIterationMethod.bind(undefined, "forEach");
|
||||
let guardedFilter = guardedIterationMethod.bind(undefined, "filter");
|
||||
let guardedMap = guardedIterationMethod.bind(undefined, "map");
|
||||
let guardedSome = guardedIterationMethod.bind(undefined, "some");
|
||||
let guardedEvery = guardedIterationMethod.bind(undefined, "every");
|
||||
let guardedFind = guardedIterationMethod.bind(undefined, "find");
|
||||
|
||||
let proxyAPI = {
|
||||
_guardedCollectionType: "array",
|
||||
_itemType: guard,
|
||||
push: guardedPush,
|
||||
reduce: guardedReduce,
|
||||
reduceRight: guardedReduceRight,
|
||||
filter: guardedFilter,
|
||||
map: guardedMap,
|
||||
forEach: guardedForEach,
|
||||
some: guardedSome,
|
||||
every: guardedEvery,
|
||||
find: guardedFind,
|
||||
unshift: guardedUnshift,
|
||||
fill: guardedFill,
|
||||
splice: guardedSplice
|
||||
};
|
||||
|
||||
let proxy = new Proxy(targetArray, {
|
||||
get: function (target, property) {
|
||||
if (proxyAPI[property] != null) {
|
||||
return proxyAPI[property];
|
||||
} else {
|
||||
return target[property];
|
||||
}
|
||||
},
|
||||
set: function (target, property, value) {
|
||||
if (indexRegex.test(property)) {
|
||||
check(value);
|
||||
}
|
||||
|
||||
target[property] = value;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
function guardedUnshift(...items) {
|
||||
for (let item of items) {
|
||||
check(item);
|
||||
}
|
||||
|
||||
return targetArray.unshift(...items);
|
||||
}
|
||||
|
||||
function guardedPush(value) {
|
||||
if (check(value) === true) {
|
||||
return targetArray.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
function guardedFill(value) {
|
||||
if (check(value) === true) {
|
||||
return targetArray.fill(value);
|
||||
}
|
||||
}
|
||||
|
||||
function guardedSplice(start, deleteCount, ...items) {
|
||||
for (let item of items) {
|
||||
check(item);
|
||||
}
|
||||
|
||||
return targetArray.splice(start, deleteCount, ...items);
|
||||
}
|
||||
|
||||
/* We provide the proxy instead of the original array as the 'array' argument for iteration functions, to prevent direct access to the underlying array; otherwise the user could accidentally mutate the underlying array from within a forEach callback, bypassing the validation requirements. */
|
||||
|
||||
function guardedIterationMethod(method, callback, thisArg) {
|
||||
return targetArray[method]((value, index, _array) => {
|
||||
return callback(value, index, proxy);
|
||||
}, thisArg);
|
||||
}
|
||||
|
||||
function guardedReduce(callback, initialValue) {
|
||||
return targetArray.reduce((accumulator, value, index, _array) => {
|
||||
return callback(accumulator, value, index, proxy);
|
||||
}, initialValue);
|
||||
}
|
||||
|
||||
function guardedReduceRight(callback, initialValue) {
|
||||
return targetArray.reduceRight((accumulator, value, index, _array) => {
|
||||
return callback(accumulator, value, index, proxy);
|
||||
}, initialValue);
|
||||
}
|
||||
|
||||
return proxy;
|
||||
} else {
|
||||
throw generateArraySignatureError();
|
||||
}
|
||||
};
|
@ -0,0 +1,46 @@
|
||||
"use strict";
|
||||
|
||||
const expect = require("chai").expect;
|
||||
|
||||
const dm = require("../src");
|
||||
|
||||
let User = dm.createType("User", {
|
||||
messages: dm.arrayOf(dm.string())
|
||||
});
|
||||
|
||||
describe("guarded arrays", () => {
|
||||
let joe = User({
|
||||
messages: []
|
||||
});
|
||||
|
||||
it("should accept valid values", () => {
|
||||
joe.messages.push("Hello world!");
|
||||
expect(joe.messages[0]).to.equal("Hello world!");
|
||||
|
||||
joe.messages.splice(0, 0, "Hello earth!")
|
||||
expect(joe.messages[0]).to.equal("Hello earth!");
|
||||
expect(joe.messages[1]).to.equal("Hello world!");
|
||||
|
||||
joe.messages[0] = "Hello moon!";
|
||||
console.log(joe.messages);
|
||||
expect(joe.messages[0]).to.equal("Hello moon!");
|
||||
expect(joe.messages[1]).to.equal("Hello world!");
|
||||
});
|
||||
|
||||
it("should reject invalid values", () => {
|
||||
expect(() => {
|
||||
joe.messages.push(42);
|
||||
}).to.throw("Expected a string, got a number instead");
|
||||
|
||||
expect(() => {
|
||||
joe.messages.splice(0, 0, 43);
|
||||
}).to.throw("Expected a string, got a number instead");
|
||||
|
||||
expect(() => {
|
||||
joe.messages[0] = 44;
|
||||
}).to.throw("Expected a string, got a number instead");
|
||||
|
||||
expect(joe.messages[0]).to.equal("Hello moon!");
|
||||
expect(joe.messages[1]).to.equal("Hello world!");
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue