Skip to content

Commit

Permalink
COMPUTED DATA PROXIES
Browse files Browse the repository at this point in the history
  • Loading branch information
zachleat committed Apr 20, 2020
1 parent 9109e76 commit 1fd4f02
Show file tree
Hide file tree
Showing 6 changed files with 341 additions and 133 deletions.
9 changes: 4 additions & 5 deletions src/ComputedData.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class ComputedData {
this.templateStringKeyLookup[key] = true;
}

async getVarOrder() {
async getVarOrder(data) {
if (this.computedKeys.size > 0) {
let graph = new DependencyGraph();

Expand All @@ -44,16 +44,15 @@ class ComputedData {
}
}

let varsUsed;
let proxy;
let isTemplateString = !!this.templateStringKeyLookup[key];
// TODO move this out of the loop??
if (isTemplateString) {
proxy = new ComputedDataTemplateString(this.computedKeys);
} else {
proxy = new ComputedDataProxy(this.computedKeys);
}
varsUsed = await proxy.findVarsUsed(computed);

let varsUsed = await proxy.findVarsUsed(computed, data);
for (let varUsed of varsUsed) {
if (varUsed !== key && this.computedKeys.has(varUsed)) {
graph.addNode(varUsed);
Expand All @@ -70,7 +69,7 @@ class ComputedData {
}

async setupData(data) {
let order = await this.getVarOrder();
let order = await this.getVarOrder(data);

for (let key of order) {
let computed = lodashGet(this.computed, key);
Expand Down
87 changes: 55 additions & 32 deletions src/ComputedDataProxy.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
const lodashSet = require("lodash/set");
const lodashGet = require("lodash/get");
const lodashIsPlainObject = require("lodash/isPlainObject");

class ComputedDataProxy {
constructor(computedKeys) {
if (Array.isArray(computedKeys)) {
Expand All @@ -7,53 +11,72 @@ class ComputedDataProxy {
}
}

_createDeepProxy(keyReference, path = []) {
let self = this;
return new Proxy(
{},
{
get(target, key) {
if (target[key]) {
return target[key];
}
let newPath = [...path, key];
if (typeof key === "string") {
// if `page.url`, remove `page` etc
let parentIndex = keyReference.indexOf(path.join("."));
if (parentIndex > -1) {
keyReference.splice(parentIndex, 1);
}
let fullPath = newPath.join(".");
keyReference.push(fullPath);

// return string for computed key values
if (self.computedKeys.has(fullPath)) {
return "";
}
isArrayOrPlainObject(data) {
return Array.isArray(data) || lodashIsPlainObject(data);
}

return self._createDeepProxy(keyReference, newPath);
}
getProxyData(data, keyRef) {
let undefinedValue = "__11TY_UNDEFINED__";
if (this.computedKeys) {
for (let key of this.computedKeys) {
if (lodashGet(data, key, undefinedValue) === undefinedValue) {
lodashSet(data, key, "");
}
}
);
}

return this._getProxyData(data, keyRef);
}

getProxyData(keyReference) {
return this._createDeepProxy(keyReference);
_getProxyData(data, keyRef, parentKey = "") {
if (lodashIsPlainObject(data)) {
return new Proxy(
{},
{
get: (obj, key) => {
// console.log( obj, key, parentKey );
if (typeof key !== "string") {
return obj[key];
}
let newKey = `${parentKey ? `${parentKey}.` : ""}${key}`;
let newData = this._getProxyData(data[key], keyRef, newKey);
if (!this.isArrayOrPlainObject(newData)) {
keyRef.add(newKey);
}
return newData;
}
}
);
} else if (Array.isArray(data)) {
return new Proxy([], {
get: (obj, key) => {
let newKey = `${parentKey}[${key}]`;
let newData = this._getProxyData(data[key], keyRef, newKey);
if (!this.isArrayOrPlainObject(newData)) {
keyRef.add(newKey);
}
return newData;
}
});
}

// everything else!
return data;
}

async findVarsUsed(fn) {
let keyReference = [];
async findVarsUsed(fn, data = {}) {
let keyRef = new Set();

// careful, logging proxyData will mess with test results!
let proxyData = this.getProxyData(keyReference);
let proxyData = this.getProxyData(data, keyRef);

// squelch console logs for this fake proxy data pass 😅
let savedLog = console.log;
console.log = () => {};
await fn(proxyData);
console.log = savedLog;

return keyReference;
return Array.from(keyRef);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/ComputedDataTemplateString.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class ComputedDataTemplateString {
return Array.from(vars);
}

async findVarsUsed(fn) {
async findVarsUsed(fn, data = {}) {
let proxyData = this.getProxyData();
let output;
let savedLog = console.log;
Expand Down
212 changes: 212 additions & 0 deletions test/ComputedDataProxyTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import test from "ava";
import ComputedDataProxy from "../src/ComputedDataProxy";

test("Get vars used by function", async t => {
let cd = new ComputedDataProxy(["key1"]);
let key1Fn = () => {};
let key2Fn = data => {
return `${data.key1}`;
};

t.deepEqual(await cd.findVarsUsed(key1Fn), []);
t.deepEqual(await cd.findVarsUsed(key2Fn), ["key1"]);
});

test("Get vars used by function (not a computed key)", async t => {
let cd = new ComputedDataProxy(["page.url"]);
let key1Fn = data => {
return `${data.page.url}`;
};

t.deepEqual(
await cd.findVarsUsed(key1Fn, {
page: { url: "" }
}),
["page.url"]
);
});

test("Get vars used by function (multiple functions—not computed keys)", async t => {
let cd = new ComputedDataProxy([
"page.url",
"key1",
"very.deep.reference",
"very.other.deep.reference"
]);

// this would be real
let sampleData = {
key1: "",
page: {
url: ""
},
very: {
deep: {
reference: ""
},
other: {
deep: {
reference: ""
}
}
}
};

let key1Fn = data => {
return `${data.page.url}`;
};
let key2Fn = data => {
return `${data.key1}${data.very.deep.reference}${data.very.other.deep.reference}`;
};

t.deepEqual(await cd.findVarsUsed(key1Fn, sampleData), ["page.url"]);
t.deepEqual(await cd.findVarsUsed(key2Fn, sampleData), [
"key1",
"very.deep.reference",
"very.other.deep.reference"
]);
});

test("Proxy shouldn’t always return {}", async t => {
let cd = new ComputedDataProxy(["page.fileSlug"]);
let proxy = cd.getProxyData(
{
page: {
fileSlug: ""
}
},
new Set()
);

t.notDeepEqual(proxy.page.fileSlug, {});
t.is(proxy.page.fileSlug, "");
});

test("isArrayOrPlainObject", async t => {
let cd = new ComputedDataProxy();

t.is(cd.isArrayOrPlainObject(true), false);
t.is(cd.isArrayOrPlainObject(false), false);
t.is(cd.isArrayOrPlainObject(1), false);
t.is(
cd.isArrayOrPlainObject(() => {}),
false
);
t.is(
cd.isArrayOrPlainObject(function() {}),
false
);
t.is(cd.isArrayOrPlainObject(new Date()), false);
t.is(cd.isArrayOrPlainObject({}), true);
t.is(cd.isArrayOrPlainObject([]), true);
});

test("findVarsUsed empty", async t => {
let cdg = new ComputedDataProxy();
t.deepEqual(await cdg.findVarsUsed(() => {}), []);
t.deepEqual(await cdg.findVarsUsed(({}) => {}), []);

let data = { key: "value" };
t.deepEqual(await cdg.findVarsUsed(data => {}), []);
t.deepEqual(await cdg.findVarsUsed(data => data.key), ["key"]);
});

test("findVarsUsed with a computed key (target a string)", async t => {
let cdg = new ComputedDataProxy();
let data = {
key: "value",
computed: {
key: function(data) {
return data.key;
}
}
};

t.deepEqual(await cdg.findVarsUsed(data.computed.key, data), ["key"]);
});

test("findVarsUsed with a computed key (target an array)", async t => {
let cdg = new ComputedDataProxy();
let data = {
arr: [0, 1, 2],
computed: {
key: function(data) {
return data.arr[1];
}
}
};

t.deepEqual(await cdg.findVarsUsed(data.computed.key, data), ["arr[1]"]);
});

test("findVarsUsed with a computed key (target an object)", async t => {
let cdg = new ComputedDataProxy();
let data = {
obj: {
b: 1
},
computed: {
key: function(data) {
return data.obj.b;
}
}
};

t.deepEqual(await cdg.findVarsUsed(data.computed.key, data), ["obj.b"]);
});

test("findVarsUsed with a computed key (target an object in an array)", async t => {
let cdg = new ComputedDataProxy();
let data = {
obj: [{ b: 1 }, { a: 2 }],
computed: {
key: function(data) {
return data.obj[1].a;
}
}
};

t.deepEqual(await cdg.findVarsUsed(data.computed.key, data), ["obj[1].a"]);
});

test("findVarsUsed with a computed key (target a string not used in the output)", async t => {
let cdg = new ComputedDataProxy();
let data = {
key1: "value1",
key2: "value2",
computed: {
key: function(data) {
let b = data.key2;
return data.key1;
}
}
};

t.deepEqual(await cdg.findVarsUsed(data.computed.key, data), [
"key2",
"key1"
]);
});

test("findVarsUsed with a deep computed reference that doesn’t exist in parent data", async t => {
let cdg = new ComputedDataProxy(["deep.deep1", "deep.deep2"]);
let data = {
key1: "value1",
key2: "value2",
computed: {
deep: {
deep1: function(data) {
return data.key2;
},
deep2: function(data) {
return data.deep.deep1;
}
}
}
};

t.deepEqual(await cdg.findVarsUsed(data.computed.deep.deep2, data), [
"deep.deep1"
]);
t.deepEqual(await cdg.findVarsUsed(data.computed.deep.deep1, data), ["key2"]);
});
Loading

0 comments on commit 1fd4f02

Please sign in to comment.