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