diff --git a/src/plugins/identity/contractIdentities.js b/src/plugins/identity/contractIdentities.js new file mode 100644 index 0000000..7f151ed --- /dev/null +++ b/src/plugins/identity/contractIdentities.js @@ -0,0 +1,67 @@ +// @flow + +import * as Weights from "../../core/weights"; +import {type WeightedGraph as WeightedGraphT} from "../../core/weightedGraph"; +import {type NodeContraction, NodeAddress} from "../../core/graph"; +import {nodeContractions} from "./nodeContractions"; +import {type Identity} from "./identity"; + +/** + * Applies nodeContractions to a WeightedGraph. + * + * This functionality is defined as part of the identity plugin rather than as + * a feature of WeightedGraph because it doesn't attempt to contract weights + * (it's not clear how to do this in a general principled way). Within the + * identity plugin, we do not expect user identities to have weights + * associated, so we can contract the graph without attempting to contract the + * weights. + * + * As a safety measure, this method will error if any of the node addresses + * being contracted has an explicitly set weight. It will not error if there + * are matching type weights, so that it is still possible (e.g.) to apply + * a weighting to an entire plugin. + * + * For more context on this decision, see discussion in #1591. + */ +export function _contractWeightedGraph( + wg: WeightedGraphT, + contractions: $ReadOnlyArray +): WeightedGraphT { + const {graph, weights} = wg; + for (const {old} of contractions) { + for (const address of old) { + const weight = weights.nodeWeights.get(address) || 0; + if (weight !== 0) { + throw new Error( + `Explicit weight ${weight} on contracted node ${NodeAddress.toString( + address + )}` + ); + } + } + } + return { + graph: graph.contractNodes(contractions), + weights: Weights.copy(weights), + }; +} + +/** + * Given a WeightedGraph and identity information, produce a contracted + * WeightedGraph where all of an identitiy's aliases have been contracted into + * a unified identity. + * + * An error will be thrown if any of the aliases had an explicitly set weight, + * since we don't currently support weight contraction. + * + * Note: This function has no unit tests, because it is a trivial composition + * of two tested functions. Thus, flow typechecking in sufficient. If adding + * any complexity to this function, please also add tests. + */ +export function contractIdentities( + wg: WeightedGraphT, + identities: $ReadOnlyArray, + discourseUrl: string | null +): WeightedGraphT { + return _contractWeightedGraph(wg, nodeContractions(identities, discourseUrl)); +} diff --git a/src/plugins/identity/contractIdentities.test.js b/src/plugins/identity/contractIdentities.test.js new file mode 100644 index 0000000..8c91b49 --- /dev/null +++ b/src/plugins/identity/contractIdentities.test.js @@ -0,0 +1,45 @@ +// @flow + +import {node} from "../../core/graphTestUtil"; +import {Graph, NodeAddress} from "../../core/graph"; +import * as Weights from "../../core/weights"; +import {_contractWeightedGraph} from "./contractIdentities"; + +describe("plugins/identity/contractIdentities", () => { + const a = node("a"); + const b = node("b"); + const c = node("c"); + const graph = () => new Graph().addNode(a).addNode(b); + const contractions = () => [{old: [a.address, b.address], replacement: c}]; + const weights = () => { + const w = Weights.empty(); + w.nodeWeights.set(NodeAddress.empty, 3); + return w; + }; + describe("_contractWeightedGraph", () => { + it("contracts the graph", () => { + const wg = {graph: graph(), weights: weights()}; + const contracted = _contractWeightedGraph(wg, contractions()); + const expected = graph().contractNodes(contractions()); + expect(expected.equals(contracted.graph)).toBe(true); + }); + it("returns a copy of the weights", () => { + const ws = weights(); + const wg = {graph: graph(), weights: ws}; + const contracted = _contractWeightedGraph(wg, contractions()); + expect(contracted.weights).not.toBe(ws); + expect(ws).toEqual(contracted.weights); + // check they can be modified independently + ws.nodeWeights.set(a.address, 5); + expect(ws).not.toEqual(contracted.weights); + }); + it("throws an error if a contracted node has an explicit weight", () => { + const ws = weights(); + ws.nodeWeights.set(a.address, 5); + const wg = {graph: graph(), weights: ws}; + expect(() => _contractWeightedGraph(wg, contractions())).toThrow( + "Explicit weight 5 on contracted node" + ); + }); + }); +});