From 0cf5923bcef54521b2c5b50c48df9d0b2f424c6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dandelion=20Man=C3=A9?= Date: Tue, 7 Aug 2018 15:24:34 -0700 Subject: [PATCH] Add dispatch methods for plugin adapters (#619) This commit adds some consistent and tested methods for getting the appropriate plugin adapter for a given Node/Edge address. There are methods for both static and dynamic adapters. In the event that more than one plugin adapter matches the given address, an error is thrown; likewise in the case where there is no matching adapter. Test plan: `yarn test` Relevant to #465 --- src/app/credExplorer/PagerankTable.js | 26 ++----- src/app/pluginAdapter.js | 58 ++++++++++++++- src/app/pluginAdapter.test.js | 101 ++++++++++++++++++++++++++ 3 files changed, 165 insertions(+), 20 deletions(-) create mode 100644 src/app/pluginAdapter.test.js diff --git a/src/app/credExplorer/PagerankTable.js b/src/app/credExplorer/PagerankTable.js index fbea0c7..f3d19e4 100644 --- a/src/app/credExplorer/PagerankTable.js +++ b/src/app/credExplorer/PagerankTable.js @@ -14,23 +14,18 @@ import type { ScoredConnection, } from "../../core/attribution/pagerankNodeDecomposition"; import type {Connection} from "../../core/attribution/graphToMarkovChain"; -import type {DynamicPluginAdapter} from "../pluginAdapter"; +import { + type DynamicPluginAdapter, + dynamicDispatchByNode, + dynamicDispatchByEdge, +} from "../pluginAdapter"; import * as NullUtil from "../../util/null"; -// TODO: Factor this out and test it (#465) export function nodeDescription( address: NodeAddressT, adapters: $ReadOnlyArray ): string { - const adapter = adapters.find((adapter) => - NodeAddress.hasPrefix(address, adapter.static().nodePrefix()) - ); - if (adapter == null) { - const result = NodeAddress.toString(address); - console.warn(`No adapter for ${result}`); - return result; - } - + const adapter = dynamicDispatchByNode(adapters, address); try { return adapter.nodeDescription(address); } catch (e) { @@ -45,14 +40,7 @@ function edgeVerb( direction: "FORWARD" | "BACKWARD", adapters: $ReadOnlyArray ): string { - const adapter = adapters.find((adapter) => - EdgeAddress.hasPrefix(address, adapter.static().edgePrefix()) - ); - if (adapter == null) { - const result = EdgeAddress.toString(address); - console.warn(`No adapter for ${result}`); - return result; - } + const adapter = dynamicDispatchByEdge(adapters, address); const edgeType = adapter .static() diff --git a/src/app/pluginAdapter.js b/src/app/pluginAdapter.js index 36d2bcc..d0bfa9a 100644 --- a/src/app/pluginAdapter.js +++ b/src/app/pluginAdapter.js @@ -1,6 +1,12 @@ // @flow -import type {Graph, NodeAddressT, EdgeAddressT} from "../core/graph"; +import { + Graph, + NodeAddress, + EdgeAddress, + type NodeAddressT, + type EdgeAddressT, +} from "../core/graph"; import type {Repo} from "../core/repo"; export type EdgeType = {| @@ -29,3 +35,53 @@ export interface DynamicPluginAdapter { nodeDescription(NodeAddressT): string; static (): StaticPluginAdapter; } + +function findUniqueMatch( + xs: $ReadOnlyArray, + predicate: (T) => boolean +): T { + const results = xs.filter(predicate); + if (results.length > 1) { + throw new Error("Multiple entities match predicate"); + } + if (results.length === 0) { + throw new Error("No entity matches predicate"); + } + return results[0]; +} + +export function staticDispatchByNode( + adapters: $ReadOnlyArray, + x: NodeAddressT +): StaticPluginAdapter { + return findUniqueMatch(adapters, (a) => + NodeAddress.hasPrefix(x, a.nodePrefix()) + ); +} + +export function staticDispatchByEdge( + adapters: $ReadOnlyArray, + x: EdgeAddressT +): StaticPluginAdapter { + return findUniqueMatch(adapters, (a) => + EdgeAddress.hasPrefix(x, a.edgePrefix()) + ); +} + +export function dynamicDispatchByNode( + adapters: $ReadOnlyArray, + x: NodeAddressT +): DynamicPluginAdapter { + return findUniqueMatch(adapters, (a) => + NodeAddress.hasPrefix(x, a.static().nodePrefix()) + ); +} + +export function dynamicDispatchByEdge( + adapters: $ReadOnlyArray, + x: EdgeAddressT +): DynamicPluginAdapter { + return findUniqueMatch(adapters, (a) => + EdgeAddress.hasPrefix(x, a.static().edgePrefix()) + ); +} diff --git a/src/app/pluginAdapter.test.js b/src/app/pluginAdapter.test.js new file mode 100644 index 0000000..cdb1d07 --- /dev/null +++ b/src/app/pluginAdapter.test.js @@ -0,0 +1,101 @@ +// @flow + +import { + Graph, + NodeAddress, + EdgeAddress, + type NodeAddressT, +} from "../core/graph"; +import { + type StaticPluginAdapter, + type DynamicPluginAdapter, + staticDispatchByNode, + staticDispatchByEdge, + dynamicDispatchByNode, + dynamicDispatchByEdge, +} from "./pluginAdapter"; + +describe("app/pluginAdapter", () => { + function example() { + const staticFooAdapter: StaticPluginAdapter = { + name: () => "foo", + nodePrefix: () => NodeAddress.fromParts(["foo"]), + edgePrefix: () => EdgeAddress.fromParts(["foo"]), + nodeTypes: () => [], + edgeTypes: () => [], + load: (_unused_repo) => Promise.resolve(dynamicFooAdapter), + }; + const dynamicFooAdapter: DynamicPluginAdapter = { + graph: () => new Graph(), + nodeDescription: (x: NodeAddressT) => NodeAddress.toString(x), + static: () => staticFooAdapter, + }; + const staticBarAdapter: StaticPluginAdapter = { + name: () => "bar", + nodePrefix: () => NodeAddress.fromParts(["bar"]), + edgePrefix: () => EdgeAddress.fromParts(["bar"]), + nodeTypes: () => [], + edgeTypes: () => [], + load: (_unused_repo) => Promise.resolve(dynamicBarAdapter), + }; + const dynamicBarAdapter: DynamicPluginAdapter = { + graph: () => new Graph(), + nodeDescription: (x) => NodeAddress.toString(x), + static: () => staticBarAdapter, + }; + const statics = [staticFooAdapter, staticBarAdapter]; + const dynamics = [dynamicFooAdapter, dynamicBarAdapter]; + return { + statics, + dynamics, + staticFooAdapter, + dynamicFooAdapter, + }; + } + + describe("dispatching", () => { + describe("error handling", () => { + // Just testing staticDispatchByNode is fine, as they all call the same + // implementation + it("errors if it cannot match", () => { + const {statics} = example(); + const zod = NodeAddress.fromParts(["zod"]); + expect(() => staticDispatchByNode(statics, zod)).toThrowError( + "No entity matches" + ); + }); + it("errors if there are multiple matches", () => { + const {staticFooAdapter} = example(); + const statics = [staticFooAdapter, staticFooAdapter]; + const foo = NodeAddress.fromParts(["foo"]); + expect(() => staticDispatchByNode(statics, foo)).toThrowError( + "Multiple entities match" + ); + }); + }); + it("staticDispatchByNode works", () => { + const {statics, staticFooAdapter} = example(); + const fooSubnode = NodeAddress.fromParts(["foo", "sub"]); + expect(staticDispatchByNode(statics, fooSubnode)).toBe(staticFooAdapter); + }); + it("staticDispatchByEdge works", () => { + const {statics, staticFooAdapter} = example(); + const fooSubedge = EdgeAddress.fromParts(["foo", "sub"]); + expect(staticDispatchByEdge(statics, fooSubedge)).toBe(staticFooAdapter); + }); + it("dynamicDispatchByNode works", () => { + const {dynamics, dynamicFooAdapter} = example(); + const fooSubnode = NodeAddress.fromParts(["foo", "sub"]); + expect(dynamicDispatchByNode(dynamics, fooSubnode)).toBe( + dynamicFooAdapter + ); + }); + it("dynamicDispatchByEdge works", () => { + const {dynamics, dynamicFooAdapter} = example(); + const fooSubedge = EdgeAddress.fromParts(["foo", "sub"]); + expect(dynamicDispatchByEdge(dynamics, fooSubedge)).toBe( + dynamicFooAdapter + ); + }); + }); +});