diff --git a/src/app/adapters/defaultPlugins.js b/src/app/adapters/defaultPlugins.js index 5c1f1b7..fb7a179 100644 --- a/src/app/adapters/defaultPlugins.js +++ b/src/app/adapters/defaultPlugins.js @@ -1,9 +1,9 @@ // @flow -import type {StaticPluginAdapter} from "./pluginAdapter"; +import {StaticAdapterSet} from "./adapterSet"; import {StaticPluginAdapter as GitAdapter} from "../../plugins/git/pluginAdapter"; import {StaticPluginAdapter as GithubAdapter} from "../../plugins/github/pluginAdapter"; -export function defaultStaticAdapters(): $ReadOnlyArray { - return [new GitAdapter(), new GithubAdapter()]; +export function defaultStaticAdapters(): StaticAdapterSet { + return new StaticAdapterSet([new GitAdapter(), new GithubAdapter()]); } diff --git a/src/app/adapters/pluginAdapter.js b/src/app/adapters/pluginAdapter.js index 3e14bb1..30528f5 100644 --- a/src/app/adapters/pluginAdapter.js +++ b/src/app/adapters/pluginAdapter.js @@ -1,12 +1,6 @@ // @flow -import { - Graph, - NodeAddress, - EdgeAddress, - type NodeAddressT, - type EdgeAddressT, -} from "../../core/graph"; +import {Graph, type NodeAddressT, type EdgeAddressT} from "../../core/graph"; import type {Repo} from "../../core/repo"; export type EdgeType = {| @@ -35,77 +29,3 @@ 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()) - ); -} - -export function findNodeType( - adapter: StaticPluginAdapter, - x: NodeAddressT -): NodeType { - if (!NodeAddress.hasPrefix(x, adapter.nodePrefix())) { - throw new Error("Trying to find NodeType from the wrong plugin adapter"); - } - return findUniqueMatch(adapter.nodeTypes(), (t) => - NodeAddress.hasPrefix(x, t.prefix) - ); -} - -export function findEdgeType( - adapter: StaticPluginAdapter, - x: EdgeAddressT -): EdgeType { - if (!EdgeAddress.hasPrefix(x, adapter.edgePrefix())) { - throw new Error("Trying to find EdgeType from the wrong plugin adapter"); - } - return findUniqueMatch(adapter.edgeTypes(), (t) => - EdgeAddress.hasPrefix(x, t.prefix) - ); -} diff --git a/src/app/adapters/pluginAdapter.test.js b/src/app/adapters/pluginAdapter.test.js deleted file mode 100644 index 753546a..0000000 --- a/src/app/adapters/pluginAdapter.test.js +++ /dev/null @@ -1,205 +0,0 @@ -// @flow - -import { - Graph, - NodeAddress, - EdgeAddress, - type NodeAddressT, -} from "../../core/graph"; -import { - type StaticPluginAdapter, - type DynamicPluginAdapter, - staticDispatchByNode, - staticDispatchByEdge, - dynamicDispatchByNode, - dynamicDispatchByEdge, - findNodeType, - findEdgeType, -} from "./pluginAdapter"; - -describe("app/adapters/pluginAdapter", () => { - function example() { - const staticFooAdapter: StaticPluginAdapter = { - name: () => "foo", - nodePrefix: () => NodeAddress.fromParts(["foo"]), - edgePrefix: () => EdgeAddress.fromParts(["foo"]), - nodeTypes: () => [ - { - name: "zap", - prefix: NodeAddress.fromParts(["foo", "zap"]), - defaultWeight: 0, - }, - { - name: "kif", - prefix: NodeAddress.fromParts(["foo", "kif"]), - defaultWeight: 0, - }, - { - name: "bad-duplicate-1", - prefix: NodeAddress.fromParts(["foo", "bad"]), - defaultWeight: 0, - }, - { - name: "bad-duplicate-2", - prefix: NodeAddress.fromParts(["foo", "bad"]), - defaultWeight: 0, - }, - ], - edgeTypes: () => [ - { - forwardName: "kifs", - backwardName: "kiffed by", - prefix: EdgeAddress.fromParts(["foo", "kif"]), - }, - { - forwardName: "zaps", - backwardName: "zapped by", - prefix: EdgeAddress.fromParts(["foo", "zap"]), - }, - { - forwardName: "bad1", - backwardName: "bad1'd by", - prefix: EdgeAddress.fromParts(["foo", "bad"]), - }, - { - forwardName: "bad2", - backwardName: "bad2'd by", - prefix: EdgeAddress.fromParts(["foo", "bad"]), - }, - ], - 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 - ); - }); - }); - - describe("findNodeType", () => { - it("works in a simple case", () => { - const {staticFooAdapter} = example(); - const kifNode = NodeAddress.fromParts(["foo", "kif", "node"]); - expect(findNodeType(staticFooAdapter, kifNode).name).toEqual("kif"); - }); - it("errors if node doesn't match the plugin", () => { - const {staticFooAdapter} = example(); - const wrongNode = NodeAddress.fromParts(["bar", "kif", "node"]); - expect(() => findNodeType(staticFooAdapter, wrongNode)).toThrowError( - "wrong plugin adapter" - ); - }); - it("errors if there's no matching type", () => { - const {staticFooAdapter} = example(); - const wrongNode = NodeAddress.fromParts(["foo", "leela", "node"]); - expect(() => findNodeType(staticFooAdapter, wrongNode)).toThrowError( - "No entity matches" - ); - }); - it("errors if there's multiple matching types", () => { - const {staticFooAdapter} = example(); - const wrongNode = NodeAddress.fromParts(["foo", "bad", "brannigan"]); - expect(() => findNodeType(staticFooAdapter, wrongNode)).toThrowError( - "Multiple entities match" - ); - }); - }); - - describe("findEdgeType", () => { - it("works in a simple case", () => { - const {staticFooAdapter} = example(); - const kifEdge = EdgeAddress.fromParts(["foo", "kif", "edge"]); - expect(findEdgeType(staticFooAdapter, kifEdge).forwardName).toEqual( - "kifs" - ); - }); - it("errors if edge doesn't match the plugin", () => { - const {staticFooAdapter} = example(); - const wrongEdge = EdgeAddress.fromParts(["bar", "kif", "edge"]); - expect(() => findEdgeType(staticFooAdapter, wrongEdge)).toThrowError( - "wrong plugin adapter" - ); - }); - it("errors if there's no matching type", () => { - const {staticFooAdapter} = example(); - const wrongEdge = EdgeAddress.fromParts(["foo", "leela", "edge"]); - expect(() => findEdgeType(staticFooAdapter, wrongEdge)).toThrowError( - "No entity matches" - ); - }); - it("errors if there's multiple matching types", () => { - const {staticFooAdapter} = example(); - const wrongEdge = EdgeAddress.fromParts(["foo", "bad", "brannigan"]); - expect(() => findEdgeType(staticFooAdapter, wrongEdge)).toThrowError( - "Multiple entities match" - ); - }); - }); -}); diff --git a/src/app/credExplorer/App.test.js b/src/app/credExplorer/App.test.js index eda4fe5..5fc604e 100644 --- a/src/app/credExplorer/App.test.js +++ b/src/app/credExplorer/App.test.js @@ -6,6 +6,7 @@ import {shallow} from "enzyme"; import {Graph} from "../../core/graph"; import {makeRepo} from "../../core/repo"; import testLocalStore from "../testLocalStore"; +import {DynamicAdapterSet, StaticAdapterSet} from "../adapters/adapterSet"; import RepositorySelect from "./RepositorySelect"; import {PagerankTable} from "./pagerankTable/Table"; @@ -65,6 +66,7 @@ describe("app/credExplorer/App", () => { }; } + const emptyAdapters = new DynamicAdapterSet(new StaticAdapterSet([]), []); const exampleStates = { uninitialized: initialState, readyToLoadGraph: (loadingState) => { @@ -79,7 +81,7 @@ describe("app/credExplorer/App", () => { initialized({ type: "READY_TO_RUN_PAGERANK", loading: loadingState, - graphWithAdapters: {graph: new Graph(), adapters: []}, + graphWithAdapters: {graph: new Graph(), adapters: emptyAdapters}, }); }, pagerankEvaluated: (loadingState) => { @@ -87,7 +89,7 @@ describe("app/credExplorer/App", () => { initialized({ type: "PAGERANK_EVALUATED", loading: loadingState, - graphWithAdapters: {graph: new Graph(), adapters: []}, + graphWithAdapters: {graph: new Graph(), adapters: emptyAdapters}, pagerankNodeDecomposition: new Map(), }); }, diff --git a/src/app/credExplorer/WeightConfig.js b/src/app/credExplorer/WeightConfig.js index bd13202..7a8daa5 100644 --- a/src/app/credExplorer/WeightConfig.js +++ b/src/app/credExplorer/WeightConfig.js @@ -27,10 +27,12 @@ type UserEdgeWeight = {|+logWeight: number, +directionality: number|}; const EDGE_WEIGHTS_KEY = "edgeWeights"; const defaultEdgeWeights = (): EdgeWeights => { const result = new Map(); - for (const adapter of defaultStaticAdapters()) { - for (const {prefix} of adapter.edgeTypes()) { - result.set(prefix, {logWeight: 0, directionality: 0.5}); + for (const {prefix} of defaultStaticAdapters().edgeTypes()) { + if (prefix === EdgeAddress.empty) { + // We haven't decided how to deal with the FallbackAdapter's fallback type. + continue; } + result.set(prefix, {logWeight: 0, directionality: 0.5}); } return result; }; @@ -40,10 +42,12 @@ type UserNodeWeight = number /* in log space */; const NODE_WEIGHTS_KEY = "nodeWeights"; const defaultNodeWeights = (): NodeWeights => { const result = new Map(); - for (const adapter of defaultStaticAdapters()) { - for (const {prefix, defaultWeight} of adapter.nodeTypes()) { - result.set(prefix, Math.log2(defaultWeight)); + for (const {prefix, defaultWeight} of defaultStaticAdapters().nodeTypes()) { + if (prefix === NodeAddress.empty) { + // We haven't decided how to deal with the FallbackAdapter's fallback type. + continue; } + result.set(prefix, Math.log2(defaultWeight)); } return result; }; diff --git a/src/app/credExplorer/pagerankTable/Connection.js b/src/app/credExplorer/pagerankTable/Connection.js index 311fa77..76eb722 100644 --- a/src/app/credExplorer/pagerankTable/Connection.js +++ b/src/app/credExplorer/pagerankTable/Connection.js @@ -6,7 +6,7 @@ import * as NullUtil from "../../../util/null"; import type {NodeAddressT} from "../../../core/graph"; import type {Connection} from "../../../core/attribution/graphToMarkovChain"; import type {ScoredConnection} from "../../../core/attribution/pagerankNodeDecomposition"; -import type {DynamicPluginAdapter} from "../../adapters/pluginAdapter"; +import {DynamicAdapterSet} from "../../adapters/adapterSet"; import { edgeVerb, @@ -115,7 +115,7 @@ export class ConnectionRow extends React.PureComponent< export class ConnectionView extends React.PureComponent<{| +connection: Connection, - +adapters: $ReadOnlyArray, + +adapters: DynamicAdapterSet, |}> { render() { const {connection, adapters} = this.props; diff --git a/src/app/credExplorer/pagerankTable/Table.js b/src/app/credExplorer/pagerankTable/Table.js index 83fd0c2..3a40622 100644 --- a/src/app/credExplorer/pagerankTable/Table.js +++ b/src/app/credExplorer/pagerankTable/Table.js @@ -5,13 +5,15 @@ import sortBy from "lodash.sortby"; import {type NodeAddressT, NodeAddress} from "../../../core/graph"; import type {PagerankNodeDecomposition} from "../../../core/attribution/pagerankNodeDecomposition"; +import {DynamicAdapterSet} from "../../adapters/adapterSet"; import type {DynamicPluginAdapter} from "../../adapters/pluginAdapter"; +import {FALLBACK_NAME} from "../../adapters/fallbackAdapter"; import {NodeRowList} from "./Node"; type PagerankTableProps = {| +pnd: PagerankNodeDecomposition, - +adapters: $ReadOnlyArray, + +adapters: DynamicAdapterSet, +maxEntriesPerList: number, |}; type PagerankTableState = {|topLevelFilter: NodeAddressT|}; @@ -69,9 +71,11 @@ export class PagerankTable extends React.PureComponent< }} > - {sortBy(adapters, (a: DynamicPluginAdapter) => a.static().name()).map( - optionGroup - )} + {sortBy(adapters.adapters(), (a: DynamicPluginAdapter) => + a.static().name() + ) + .filter((a) => a.static().name() !== FALLBACK_NAME) + .map(optionGroup)} ); diff --git a/src/app/credExplorer/pagerankTable/shared.js b/src/app/credExplorer/pagerankTable/shared.js index 1077c74..576f73e 100644 --- a/src/app/credExplorer/pagerankTable/shared.js +++ b/src/app/credExplorer/pagerankTable/shared.js @@ -6,20 +6,15 @@ import { NodeAddress, } from "../../../core/graph"; -import type {PagerankNodeDecomposition} from "../../../core/attribution/pagerankNodeDecomposition"; +import {DynamicAdapterSet} from "../../adapters/adapterSet"; -import { - type DynamicPluginAdapter, - dynamicDispatchByNode, - dynamicDispatchByEdge, - findEdgeType, -} from "../../adapters/pluginAdapter"; +import type {PagerankNodeDecomposition} from "../../../core/attribution/pagerankNodeDecomposition"; export function nodeDescription( address: NodeAddressT, - adapters: $ReadOnlyArray + adapters: DynamicAdapterSet ): string { - const adapter = dynamicDispatchByNode(adapters, address); + const adapter = adapters.adapterMatchingNode(address); try { return adapter.nodeDescription(address); } catch (e) { @@ -32,10 +27,9 @@ export function nodeDescription( export function edgeVerb( address: EdgeAddressT, direction: "FORWARD" | "BACKWARD", - adapters: $ReadOnlyArray + adapters: DynamicAdapterSet ): string { - const adapter = dynamicDispatchByEdge(adapters, address); - const edgeType = findEdgeType(adapter.static(), address); + const edgeType = adapters.static().typeMatchingEdge(address); return direction === "FORWARD" ? edgeType.forwardName : edgeType.backwardName; } @@ -45,7 +39,7 @@ export function scoreDisplay(score: number) { export type SharedProps = {| +pnd: PagerankNodeDecomposition, - +adapters: $ReadOnlyArray, + +adapters: DynamicAdapterSet, +maxEntriesPerList: number, |}; diff --git a/src/app/credExplorer/pagerankTable/sharedTestUtils.js b/src/app/credExplorer/pagerankTable/sharedTestUtils.js index a6bdaaf..2a7660d 100644 --- a/src/app/credExplorer/pagerankTable/sharedTestUtils.js +++ b/src/app/credExplorer/pagerankTable/sharedTestUtils.js @@ -2,6 +2,7 @@ import {Graph, NodeAddress, EdgeAddress} from "../../../core/graph"; +import {StaticAdapterSet, DynamicAdapterSet} from "../../adapters/adapterSet"; import type {DynamicPluginAdapter} from "../../adapters/pluginAdapter"; import {pagerank} from "../../../core/attribution/pagerank"; @@ -34,7 +35,7 @@ export async function example() { barF: addEdge(["bar", "f"], nodes.bar1, nodes.xox), }; - const adapters: DynamicPluginAdapter[] = [ + const dynamicAdapters: DynamicPluginAdapter[] = [ { static: () => ({ name: () => "foo", @@ -132,6 +133,12 @@ export async function example() { }, ]; + const staticAdapters = dynamicAdapters.map((x) => x.static()); + const adapters = new DynamicAdapterSet( + new StaticAdapterSet(staticAdapters), + dynamicAdapters + ); + const pnd = await pagerank(graph, (_unused_Edge) => ({ toWeight: 1, froWeight: 1, diff --git a/src/app/credExplorer/state.js b/src/app/credExplorer/state.js index 7b9637f..84ceef6 100644 --- a/src/app/credExplorer/state.js +++ b/src/app/credExplorer/state.js @@ -12,7 +12,7 @@ import { pagerank, } from "../../core/attribution/pagerank"; -import type {DynamicPluginAdapter} from "../adapters/pluginAdapter"; +import {DynamicAdapterSet} from "../adapters/adapterSet"; import {defaultStaticAdapters} from "../adapters/defaultPlugins"; @@ -247,12 +247,11 @@ export class StateTransitionMachine implements StateTransitionMachineInterface { export type GraphWithAdapters = {| +graph: Graph, - +adapters: $ReadOnlyArray, + +adapters: DynamicAdapterSet, |}; -export function loadGraphWithAdapters(repo: Repo): Promise { - const statics = defaultStaticAdapters(); - return Promise.all(statics.map((a) => a.load(repo))).then((adapters) => { - const graph = Graph.merge(adapters.map((x) => x.graph())); - return {graph, adapters}; - }); +export async function loadGraphWithAdapters( + repo: Repo +): Promise { + const adapters = await defaultStaticAdapters().load(repo); + return {graph: adapters.graph(), adapters}; } diff --git a/src/app/credExplorer/state.test.js b/src/app/credExplorer/state.test.js index 472bca4..a6e53f2 100644 --- a/src/app/credExplorer/state.test.js +++ b/src/app/credExplorer/state.test.js @@ -10,6 +10,7 @@ import { import {Graph} from "../../core/graph"; import {makeRepo, type Repo} from "../../core/repo"; import {type EdgeEvaluator} from "../../core/attribution/pagerank"; +import {StaticAdapterSet, DynamicAdapterSet} from "../adapters/adapterSet"; import type { PagerankNodeDecomposition, PagerankOptions, @@ -66,7 +67,10 @@ describe("app/credExplorer/state", () => { return (_unused_Edge) => ({toWeight: 3, froWeight: 4}); } function graphWithAdapters(): GraphWithAdapters { - return {graph: new Graph(), adapters: []}; + return { + graph: new Graph(), + adapters: new DynamicAdapterSet(new StaticAdapterSet([]), []), + }; } function pagerankNodeDecomposition() { return new Map();