Use AdapterSets in the cred explorer (#642)

This takes the code from #640 and puts it into production.

Test plan: Unit tests pass. The observable behavior in the cred explorer
is unchanged; i.e. the addition of the FallbackAdapter did not produce
new entries in the WeightConfig or in the Pagerank table options. The
WeightConfig is untested, so we don't have verification of that behavior
(other than that I tested it and am reporting it here). The
PagerankTable code is tested, and a snapshot would fail if another
option group had appeared.
This commit is contained in:
Dandelion Mané 2018-08-10 18:19:54 -07:00 committed by GitHub
parent 9edd7ac069
commit 05c9f81cc0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 55 additions and 326 deletions

View File

@ -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<StaticPluginAdapter> {
return [new GitAdapter(), new GithubAdapter()];
export function defaultStaticAdapters(): StaticAdapterSet {
return new StaticAdapterSet([new GitAdapter(), new GithubAdapter()]);
}

View File

@ -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<T>(
xs: $ReadOnlyArray<T>,
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<StaticPluginAdapter>,
x: NodeAddressT
): StaticPluginAdapter {
return findUniqueMatch(adapters, (a) =>
NodeAddress.hasPrefix(x, a.nodePrefix())
);
}
export function staticDispatchByEdge(
adapters: $ReadOnlyArray<StaticPluginAdapter>,
x: EdgeAddressT
): StaticPluginAdapter {
return findUniqueMatch(adapters, (a) =>
EdgeAddress.hasPrefix(x, a.edgePrefix())
);
}
export function dynamicDispatchByNode(
adapters: $ReadOnlyArray<DynamicPluginAdapter>,
x: NodeAddressT
): DynamicPluginAdapter {
return findUniqueMatch(adapters, (a) =>
NodeAddress.hasPrefix(x, a.static().nodePrefix())
);
}
export function dynamicDispatchByEdge(
adapters: $ReadOnlyArray<DynamicPluginAdapter>,
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)
);
}

View File

@ -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"
);
});
});
});

View File

@ -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(),
});
},

View File

@ -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;
};

View File

@ -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<DynamicPluginAdapter>,
+adapters: DynamicAdapterSet,
|}> {
render() {
const {connection, adapters} = this.props;

View File

@ -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<DynamicPluginAdapter>,
+adapters: DynamicAdapterSet,
+maxEntriesPerList: number,
|};
type PagerankTableState = {|topLevelFilter: NodeAddressT|};
@ -69,9 +71,11 @@ export class PagerankTable extends React.PureComponent<
}}
>
<option value={NodeAddress.empty}>Show all</option>
{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)}
</select>
</label>
);

View File

@ -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<DynamicPluginAdapter>
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<DynamicPluginAdapter>
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<DynamicPluginAdapter>,
+adapters: DynamicAdapterSet,
+maxEntriesPerList: number,
|};

View File

@ -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,

View File

@ -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<DynamicPluginAdapter>,
+adapters: DynamicAdapterSet,
|};
export function loadGraphWithAdapters(repo: Repo): Promise<GraphWithAdapters> {
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<GraphWithAdapters> {
const adapters = await defaultStaticAdapters().load(repo);
return {graph: adapters.graph(), adapters};
}

View File

@ -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();