From b40c1d078d384b39cc4d96f04dc2b93127dee5ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dandelion=20Man=C3=A9?= Date: Wed, 5 Sep 2018 17:34:29 -0700 Subject: [PATCH] Create a `weights` module with types and utils (#787) This commit creates a central `weights` module that defines all of the weight-related types, and provides some utilities for dealing with them. This way users of weight-concepts do not need to depend on a lot of random modules just to get the relevant types. The utility methods are implicitly defined a few places in the codebase: now we can avoid re-writing them, and test them more thoroughly. Test plan: Unit tests pass. --- src/app/credExplorer/WeightConfig.js | 43 ++++----- .../credExplorer/weights/EdgeTypeConfig.js | 15 +-- .../weights/EdgeTypeConfig.test.js | 13 +-- .../credExplorer/weights/NodeTypeConfig.js | 11 +-- .../weights/NodeTypeConfig.test.js | 8 +- .../weights/PluginWeightConfig.js | 24 ++--- .../weights/PluginWeightConfig.test.js | 14 +-- src/app/credExplorer/weights/weights.js | 74 +++++++++++++++ src/app/credExplorer/weights/weights.test.js | 93 +++++++++++++++++++ .../weights/weightsToEdgeEvaluator.js | 2 +- 10 files changed, 205 insertions(+), 92 deletions(-) create mode 100644 src/app/credExplorer/weights/weights.js create mode 100644 src/app/credExplorer/weights/weights.test.js diff --git a/src/app/credExplorer/WeightConfig.js b/src/app/credExplorer/WeightConfig.js index 977055a..76e1948 100644 --- a/src/app/credExplorer/WeightConfig.js +++ b/src/app/credExplorer/WeightConfig.js @@ -1,22 +1,18 @@ // @flow import React from "react"; +import * as NullUtil from "../../util/null"; import {type EdgeEvaluator} from "../../core/attribution/pagerank"; import {weightsToEdgeEvaluator} from "./weights/weightsToEdgeEvaluator"; +import type {StaticPluginAdapter} from "../adapters/pluginAdapter"; import type {StaticAdapterSet} from "../adapters/adapterSet"; import { type WeightedTypes, - PluginWeightConfig, -} from "./weights/PluginWeightConfig"; -import { - type WeightedNodeType, - defaultWeightedNodeType, -} from "./weights/NodeTypeConfig"; -import { - type WeightedEdgeType, - defaultWeightedEdgeType, -} from "./weights/EdgeTypeConfig"; + defaultWeightsForAdapter, + combineWeights, +} from "./weights/weights"; +import {PluginWeightConfig} from "./weights/PluginWeightConfig"; import {FALLBACK_NAME} from "../adapters/fallbackAdapter"; type Props = {| @@ -88,23 +84,16 @@ export class WeightConfig extends React.Component { } fire() { - let nodeWeights: WeightedNodeType[] = []; - let edgeWeights: WeightedEdgeType[] = []; - for (const adapter of this.props.adapters.adapters()) { - const weights = this.state.pluginNameToWeights.get(adapter.name()); - const newNodeWeights = - weights == null - ? adapter.nodeTypes().map(defaultWeightedNodeType) - : weights.nodes; - const newEdgeWeights = - weights == null - ? adapter.edgeTypes().map(defaultWeightedEdgeType) - : weights.edges; - nodeWeights = nodeWeights.concat(newNodeWeights); - edgeWeights = edgeWeights.concat(newEdgeWeights); - } - - const weights = {nodes: nodeWeights, edges: edgeWeights}; + const weights = combineWeights( + this.props.adapters + .adapters() + .map((adapter: StaticPluginAdapter) => + NullUtil.orElse( + this.state.pluginNameToWeights.get(adapter.name()), + defaultWeightsForAdapter(adapter) + ) + ) + ); const edgeEvaluator = weightsToEdgeEvaluator(weights); this.props.onChange(edgeEvaluator); } diff --git a/src/app/credExplorer/weights/EdgeTypeConfig.js b/src/app/credExplorer/weights/EdgeTypeConfig.js index 7fc00bd..2c53dde 100644 --- a/src/app/credExplorer/weights/EdgeTypeConfig.js +++ b/src/app/credExplorer/weights/EdgeTypeConfig.js @@ -2,21 +2,8 @@ import React from "react"; import {WeightSlider, type Props as WeightSliderProps} from "./WeightSlider"; -import {type EdgeType} from "../../adapters/pluginAdapter"; -export type WeightedEdgeType = {| - +type: EdgeType, - +forwardWeight: number, - +backwardWeight: number, -|}; - -export function defaultWeightedEdgeType(type: EdgeType): WeightedEdgeType { - return { - type, - forwardWeight: type.defaultForwardWeight, - backwardWeight: type.defaultBackwardWeight, - }; -} +import type {WeightedEdgeType} from "./weights"; export class EdgeTypeConfig extends React.Component<{ +weightedType: WeightedEdgeType, diff --git a/src/app/credExplorer/weights/EdgeTypeConfig.test.js b/src/app/credExplorer/weights/EdgeTypeConfig.test.js index 3af91e3..f26f8ca 100644 --- a/src/app/credExplorer/weights/EdgeTypeConfig.test.js +++ b/src/app/credExplorer/weights/EdgeTypeConfig.test.js @@ -4,23 +4,12 @@ import React from "react"; import {shallow} from "enzyme"; import {WeightSlider} from "./WeightSlider"; -import { - defaultWeightedEdgeType, - EdgeTypeConfig, - EdgeWeightSlider, -} from "./EdgeTypeConfig"; +import {EdgeTypeConfig, EdgeWeightSlider} from "./EdgeTypeConfig"; import {assemblesEdgeType} from "../../adapters/demoAdapters"; require("../../testUtil").configureEnzyme(); describe("app/credExplorer/weights/EdgeTypeConfig", () => { - describe("defaultWeightedEdgeType", () => { - it("sets default weights as specified in the type", () => { - const wet = defaultWeightedEdgeType(assemblesEdgeType); - expect(wet.forwardWeight).toEqual(wet.type.defaultForwardWeight); - expect(wet.backwardWeight).toEqual(wet.type.defaultBackwardWeight); - }); - }); describe("EdgeTypeConfig", () => { function example() { const onChange = jest.fn(); diff --git a/src/app/credExplorer/weights/NodeTypeConfig.js b/src/app/credExplorer/weights/NodeTypeConfig.js index f22dcff..12276c4 100644 --- a/src/app/credExplorer/weights/NodeTypeConfig.js +++ b/src/app/credExplorer/weights/NodeTypeConfig.js @@ -2,16 +2,7 @@ import React from "react"; import {WeightSlider} from "./WeightSlider"; -import {type NodeType} from "../../adapters/pluginAdapter"; - -export type WeightedNodeType = {|+type: NodeType, +weight: number|}; - -export function defaultWeightedNodeType(type: NodeType): WeightedNodeType { - return { - type, - weight: type.defaultWeight, - }; -} +import type {WeightedNodeType} from "./weights"; export class NodeTypeConfig extends React.Component<{ +weightedType: WeightedNodeType, diff --git a/src/app/credExplorer/weights/NodeTypeConfig.test.js b/src/app/credExplorer/weights/NodeTypeConfig.test.js index c1c33e5..6749796 100644 --- a/src/app/credExplorer/weights/NodeTypeConfig.test.js +++ b/src/app/credExplorer/weights/NodeTypeConfig.test.js @@ -4,18 +4,12 @@ import React from "react"; import {shallow} from "enzyme"; import {WeightSlider} from "./WeightSlider"; -import {defaultWeightedNodeType, NodeTypeConfig} from "./NodeTypeConfig"; +import {NodeTypeConfig} from "./NodeTypeConfig"; import {inserterNodeType} from "../../adapters/demoAdapters"; require("../../testUtil").configureEnzyme(); describe("app/credExplorer/weights/NodeTypeConfig", () => { - describe("defaultWeightedNodeType", () => { - it("sets default weight as specified in type", () => { - const wnt = defaultWeightedNodeType(inserterNodeType); - expect(wnt.weight).toEqual(wnt.type.defaultWeight); - }); - }); describe("NodeTypeConfig", () => { function example() { const onChange = jest.fn(); diff --git a/src/app/credExplorer/weights/PluginWeightConfig.js b/src/app/credExplorer/weights/PluginWeightConfig.js index f0fb691..1b46d76 100644 --- a/src/app/credExplorer/weights/PluginWeightConfig.js +++ b/src/app/credExplorer/weights/PluginWeightConfig.js @@ -2,23 +2,17 @@ import React from "react"; import deepEqual from "lodash.isequal"; -import { - NodeTypeConfig, - defaultWeightedNodeType, - type WeightedNodeType, -} from "./NodeTypeConfig"; -import { - EdgeTypeConfig, - defaultWeightedEdgeType, - type WeightedEdgeType, -} from "./EdgeTypeConfig"; +import {NodeTypeConfig} from "./NodeTypeConfig"; +import {EdgeTypeConfig} from "./EdgeTypeConfig"; import {StaticPluginAdapter} from "../../adapters/pluginAdapter"; import {styledVariable} from "./EdgeTypeConfig"; - -export type WeightedTypes = {| - +nodes: $ReadOnlyArray, - +edges: $ReadOnlyArray, -|}; +import { + type WeightedTypes, + type WeightedEdgeType, + type WeightedNodeType, + defaultWeightedNodeType, + defaultWeightedEdgeType, +} from "./weights"; export type Props = {| +adapter: StaticPluginAdapter, diff --git a/src/app/credExplorer/weights/PluginWeightConfig.test.js b/src/app/credExplorer/weights/PluginWeightConfig.test.js index d6609dc..77fe64f 100644 --- a/src/app/credExplorer/weights/PluginWeightConfig.test.js +++ b/src/app/credExplorer/weights/PluginWeightConfig.test.js @@ -4,8 +4,13 @@ import React from "react"; import {shallow} from "enzyme"; import {PluginWeightConfig} from "./PluginWeightConfig"; import {FactorioStaticAdapter} from "../../adapters/demoAdapters"; -import {NodeTypeConfig, defaultWeightedNodeType} from "./NodeTypeConfig"; -import {EdgeTypeConfig, defaultWeightedEdgeType} from "./EdgeTypeConfig"; +import {NodeTypeConfig} from "./NodeTypeConfig"; +import {EdgeTypeConfig} from "./EdgeTypeConfig"; +import { + defaultWeightsForAdapter, + defaultWeightedNodeType, + defaultWeightedEdgeType, +} from "./weights"; require("../../testUtil").configureEnzyme(); @@ -21,10 +26,7 @@ describe("src/app/credExplorer/weights/PluginWeightConfig", () => { } it("fires plugin's default weights on mount", () => { const {onChange, adapter} = example(); - const expected = { - nodes: adapter.nodeTypes().map(defaultWeightedNodeType), - edges: adapter.edgeTypes().map(defaultWeightedEdgeType), - }; + const expected = defaultWeightsForAdapter(adapter); expect(onChange).toHaveBeenCalledWith(expected); }); it("renders a NodeTypeConfig for each node type", () => { diff --git a/src/app/credExplorer/weights/weights.js b/src/app/credExplorer/weights/weights.js new file mode 100644 index 0000000..95a65f1 --- /dev/null +++ b/src/app/credExplorer/weights/weights.js @@ -0,0 +1,74 @@ +// @flow + +import {NodeAddress, EdgeAddress} from "../../../core/graph"; +import type {NodeType, EdgeType} from "../../adapters/pluginAdapter"; +import type {StaticPluginAdapter} from "../../adapters/pluginAdapter"; +import type {StaticAdapterSet} from "../../adapters/adapterSet"; + +export type WeightedNodeType = {|+type: NodeType, +weight: number|}; + +export type WeightedEdgeType = {| + +type: EdgeType, + +forwardWeight: number, + +backwardWeight: number, +|}; + +export type WeightedTypes = {| + +nodes: $ReadOnlyArray, + +edges: $ReadOnlyArray, +|}; + +export function defaultWeightedNodeType(type: NodeType): WeightedNodeType { + return { + type, + weight: type.defaultWeight, + }; +} + +export function defaultWeightedEdgeType(type: EdgeType): WeightedEdgeType { + return { + type, + forwardWeight: type.defaultForwardWeight, + backwardWeight: type.defaultBackwardWeight, + }; +} + +export function defaultWeightsForAdapter( + adapter: StaticPluginAdapter +): WeightedTypes { + return { + nodes: adapter.nodeTypes().map(defaultWeightedNodeType), + edges: adapter.edgeTypes().map(defaultWeightedEdgeType), + }; +} + +export function combineWeights( + ws: $ReadOnlyArray +): WeightedTypes { + const seenPrefixes = new Set(); + const nodes = [].concat(...ws.map((x) => x.nodes)); + for (const { + type: {prefix}, + } of nodes) { + if (seenPrefixes.has(prefix)) { + throw new Error(`Duplicate prefix: ${NodeAddress.toString(prefix)}`); + } + seenPrefixes.add(prefix); + } + const edges = [].concat(...ws.map((x) => x.edges)); + for (const { + type: {prefix}, + } of edges) { + if (seenPrefixes.has(prefix)) { + throw new Error(`Duplicate prefix: ${EdgeAddress.toString(prefix)}`); + } + seenPrefixes.add(prefix); + } + return {nodes, edges}; +} + +export function defaultWeightsForAdapterSet( + adapters: StaticAdapterSet +): WeightedTypes { + return combineWeights(adapters.adapters().map(defaultWeightsForAdapter)); +} diff --git a/src/app/credExplorer/weights/weights.test.js b/src/app/credExplorer/weights/weights.test.js new file mode 100644 index 0000000..48ba0da --- /dev/null +++ b/src/app/credExplorer/weights/weights.test.js @@ -0,0 +1,93 @@ +// @flow + +import { + defaultWeightedNodeType, + defaultWeightedEdgeType, + defaultWeightsForAdapter, + combineWeights, + defaultWeightsForAdapterSet, +} from "./weights"; +import { + inserterNodeType, + machineNodeType, + assemblesEdgeType, + transportsEdgeType, + FactorioStaticAdapter, + staticAdapterSet, +} from "../../adapters/demoAdapters"; + +describe("app/credExplorer/weights/weights", () => { + describe("defaultWeightedNodeType", () => { + it("sets default weight as specified in type", () => { + const wnt = defaultWeightedNodeType(inserterNodeType); + expect(wnt.weight).toEqual(wnt.type.defaultWeight); + }); + }); + describe("defaultWeightedEdgeType", () => { + it("sets default weights as specified in the type", () => { + const wet = defaultWeightedEdgeType(assemblesEdgeType); + expect(wet.forwardWeight).toEqual(wet.type.defaultForwardWeight); + expect(wet.backwardWeight).toEqual(wet.type.defaultBackwardWeight); + }); + }); + describe("defaultWeightsForAdapter", () => { + it("works on the demo adapter", () => { + const adapter = new FactorioStaticAdapter(); + const expected = { + nodes: adapter.nodeTypes().map(defaultWeightedNodeType), + edges: adapter.edgeTypes().map(defaultWeightedEdgeType), + }; + expect(defaultWeightsForAdapter(adapter)).toEqual(expected); + }); + }); + describe("combineWeights", () => { + const defaultWeights = () => + defaultWeightsForAdapter(new FactorioStaticAdapter()); + const emptyWeights = () => ({nodes: [], edges: []}); + it("successfully combines WeightedTypes", () => { + const weights1 = { + nodes: [defaultWeightedNodeType(inserterNodeType)], + edges: [defaultWeightedEdgeType(assemblesEdgeType)], + }; + const weights2 = { + nodes: [defaultWeightedNodeType(machineNodeType)], + edges: [defaultWeightedEdgeType(transportsEdgeType)], + }; + expect(combineWeights([weights1, weights2])).toEqual(defaultWeights()); + }); + it("treats empty weights as an identity", () => { + expect( + combineWeights([emptyWeights(), defaultWeights(), emptyWeights()]) + ).toEqual(defaultWeights()); + }); + it("errors on duplicate edge prefix", () => { + const weights = { + nodes: [], + edges: [defaultWeightedEdgeType(assemblesEdgeType)], + }; + expect(() => combineWeights([weights, weights])).toThrowError( + "Duplicate prefix" + ); + }); + it("errors on duplicate node prefix", () => { + const weights = { + nodes: [defaultWeightedNodeType(inserterNodeType)], + edges: [], + }; + expect(() => combineWeights([weights, weights])).toThrowError( + "Duplicate prefix" + ); + }); + }); + describe("defaultWeightsForAdapterSet", () => { + it("works on a demo adapter set", () => { + expect(defaultWeightsForAdapterSet(staticAdapterSet())).toEqual( + combineWeights( + staticAdapterSet() + .adapters() + .map(defaultWeightsForAdapter) + ) + ); + }); + }); +}); diff --git a/src/app/credExplorer/weights/weightsToEdgeEvaluator.js b/src/app/credExplorer/weights/weightsToEdgeEvaluator.js index fd373dc..a84fdc1 100644 --- a/src/app/credExplorer/weights/weightsToEdgeEvaluator.js +++ b/src/app/credExplorer/weights/weightsToEdgeEvaluator.js @@ -1,7 +1,7 @@ // @flow import type {Edge} from "../../../core/graph"; -import type {WeightedTypes} from "./PluginWeightConfig"; +import type {WeightedTypes} from "./weights"; import type {EdgeEvaluator} from "../../../core/attribution/pagerank"; import {NodeTrie, EdgeTrie} from "../../../core/trie";