Use a Symbol for DelegateNodeReference base ref (#313)

Summary:
It’s critical, even more so than usual, that the “base reference”
property of a `DelegateNodeReference` be a private property, because
this class is designed for inheritance. In ECMAScript 6, we can achieve
this by giving the property a `Symbol` key instead of a string key.
Unfortunately, Flow doesn’t know about `Symbol`s, so we need a few casts
through `any`, but they are localized to as small a scope as possible.

Test Plan:
Unit tests added. Note that they pass both before and after this change.

wchargin-branch: symbol-base-ref
This commit is contained in:
William Chargin 2018-05-29 12:28:34 -07:00 committed by GitHub
parent 6663c4f8ad
commit 87b7df5957
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 69 additions and 8 deletions

View File

@ -276,23 +276,28 @@ function findHandler(pluginMap: PluginMap, pluginName: string) {
return pluginMap[pluginName];
}
const DELEGATE_NODE_REFERENCE_BASE = Symbol("base");
function getBase(dnr: DelegateNodeReference): NodeReference {
// Flow doesn't know about Symbols, so we use this function to
// localize the `any`-casts as much as possible.
return (dnr: any)[DELEGATE_NODE_REFERENCE_BASE];
}
export class DelegateNodeReference implements NodeReference {
// TODO(@wchargin): Use a Symbol here.
__DelegateNodeReference_base: NodeReference;
constructor(base: NodeReference) {
this.__DelegateNodeReference_base = base;
(this: any)[DELEGATE_NODE_REFERENCE_BASE] = base;
}
graph() {
return this.__DelegateNodeReference_base.graph();
return getBase(this).graph();
}
address() {
return this.__DelegateNodeReference_base.address();
return getBase(this).address();
}
get() {
return this.__DelegateNodeReference_base.get();
return getBase(this).get();
}
neighbors(options?: NeighborsOptions) {
return this.__DelegateNodeReference_base.neighbors(options);
return getBase(this).neighbors(options);
}
}

View File

@ -3,7 +3,7 @@ import stringify from "json-stable-stringify";
import sortBy from "lodash.sortby";
import type {Node} from "./graph";
import {Graph} from "./graph";
import {DelegateNodeReference, Graph} from "./graph";
import {
FooPayload,
@ -213,3 +213,59 @@ describe("graph", () => {
});
});
});
describe("DelegateNodeReference", () => {
const makeBase = () => ({
graph: jest.fn(),
address: jest.fn(),
get: jest.fn(),
neighbors: jest.fn(),
});
it("has a working constructor", () => {
expect(new DelegateNodeReference(makeBase())).toBeInstanceOf(
DelegateNodeReference
);
});
it("delegates `graph`", () => {
const expected = new Graph([]);
const ref = {
...makeBase(),
graph: jest.fn().mockReturnValueOnce(expected),
};
expect(new DelegateNodeReference(ref).graph()).toBe(expected);
expect(ref.graph.mock.calls).toEqual([[]]);
});
it("delegates `address`", () => {
const expected = {owner: {plugin: "foo", type: "bar"}, id: "baz"};
const ref = {
...makeBase(),
address: jest.fn().mockReturnValueOnce(expected),
};
expect(new DelegateNodeReference(ref).address()).toBe(expected);
expect(ref.address.mock.calls).toEqual([[]]);
});
it("delegates `get`", () => {
const expected = {some: "node"};
const ref = {
...makeBase(),
get: jest.fn().mockReturnValueOnce((expected: any)),
};
expect(new DelegateNodeReference(ref).get()).toBe(expected);
expect(ref.get.mock.calls).toEqual([[]]);
});
it("delegates `neighbors`, with proper options", () => {
const options = {direction: "OUT", node: {plugin: "foo", type: "bar"}};
const ref = {
...makeBase(),
neighbors: jest.fn().mockImplementationOnce((...args) => (args: any)),
};
const result = new DelegateNodeReference(ref).neighbors(options);
expect(result).toEqual([options]);
expect(ref.neighbors.mock.calls).toEqual([[options]]);
});
});