Initiatives: implement basic createGraph (#1477)
This commit is contained in:
parent
579b01ed46
commit
b05cc84f2e
|
@ -0,0 +1,98 @@
|
|||
// @flow
|
||||
|
||||
import {
|
||||
Graph,
|
||||
EdgeAddress,
|
||||
NodeAddress,
|
||||
type Edge,
|
||||
type Node,
|
||||
type EdgeAddressT,
|
||||
type NodeAddressT,
|
||||
} from "../../core/graph";
|
||||
import type {ReferenceDetector, URL} from "../../core/references";
|
||||
import type {Initiative, InitiativeRepository} from "./initiative";
|
||||
import {
|
||||
initiativeNodeType,
|
||||
dependsOnEdgeType,
|
||||
referencesEdgeType,
|
||||
contributesToEdgeType,
|
||||
championsEdgeType,
|
||||
} from "./declaration";
|
||||
|
||||
function initiativeAddress(initiative: Initiative): NodeAddressT {
|
||||
return NodeAddress.append(
|
||||
initiativeNodeType.prefix,
|
||||
...NodeAddress.toParts(initiative.tracker)
|
||||
);
|
||||
}
|
||||
|
||||
function initiativeNode(initiative: Initiative): Node {
|
||||
return {
|
||||
address: initiativeAddress(initiative),
|
||||
timestampMs: initiative.timestampMs,
|
||||
description: initiative.title,
|
||||
};
|
||||
}
|
||||
|
||||
type EdgeFactoryT = (initiative: Initiative, other: NodeAddressT) => Edge;
|
||||
|
||||
function edgeFactory(
|
||||
prefix: EdgeAddressT,
|
||||
fromInitiative: boolean
|
||||
): EdgeFactoryT {
|
||||
return (initiative: Initiative, other: NodeAddressT): Edge => {
|
||||
const iAddr = initiativeAddress(initiative);
|
||||
const src = fromInitiative ? iAddr : other;
|
||||
const dst = fromInitiative ? other : iAddr;
|
||||
return {
|
||||
address: EdgeAddress.append(
|
||||
prefix,
|
||||
...NodeAddress.toParts(initiativeAddress(initiative)),
|
||||
...NodeAddress.toParts(other)
|
||||
),
|
||||
timestampMs: initiative.timestampMs,
|
||||
src,
|
||||
dst,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const depedencyEdge = edgeFactory(dependsOnEdgeType.prefix, true);
|
||||
const referenceEdge = edgeFactory(referencesEdgeType.prefix, true);
|
||||
const contributionEdge = edgeFactory(contributesToEdgeType.prefix, false);
|
||||
const championEdge = edgeFactory(championsEdgeType.prefix, false);
|
||||
|
||||
export function createGraph(
|
||||
repo: InitiativeRepository,
|
||||
refs: ReferenceDetector
|
||||
): Graph {
|
||||
const graph = new Graph();
|
||||
|
||||
for (const initiative of repo.initiatives()) {
|
||||
// Adds the Initiative node.
|
||||
graph.addNode(initiativeNode(initiative));
|
||||
|
||||
// Consider the tracker a contribution.
|
||||
graph.addEdge(contributionEdge(initiative, initiative.tracker));
|
||||
|
||||
// Generic approach to adding edges when the reference detector has a hit.
|
||||
const edgeHandler = (
|
||||
urls: $ReadOnlyArray<URL>,
|
||||
createEdge: EdgeFactoryT
|
||||
) => {
|
||||
for (const url of urls) {
|
||||
const addr = refs.addressFromUrl(url);
|
||||
if (!addr) continue;
|
||||
graph.addEdge(createEdge(initiative, addr));
|
||||
}
|
||||
};
|
||||
|
||||
// Maps the edge types to it's fields.
|
||||
edgeHandler(initiative.dependencies, depedencyEdge);
|
||||
edgeHandler(initiative.references, referenceEdge);
|
||||
edgeHandler(initiative.contributions, contributionEdge);
|
||||
edgeHandler(initiative.champions, championEdge);
|
||||
}
|
||||
|
||||
return graph;
|
||||
}
|
|
@ -0,0 +1,357 @@
|
|||
// @flow
|
||||
|
||||
import {
|
||||
EdgeAddress,
|
||||
NodeAddress,
|
||||
type EdgeAddressT,
|
||||
type NodeAddressT,
|
||||
} from "../../core/graph";
|
||||
import type {ReferenceDetector, URL} from "../../core/references";
|
||||
import type {Initiative, InitiativeRepository} from "./initiative";
|
||||
import {topicAddress} from "../discourse/address";
|
||||
import {createGraph} from "./createGraph";
|
||||
import {
|
||||
initiativeNodeType,
|
||||
dependsOnEdgeType,
|
||||
referencesEdgeType,
|
||||
contributesToEdgeType,
|
||||
championsEdgeType,
|
||||
} from "./declaration";
|
||||
|
||||
class MockInitiativeRepository implements InitiativeRepository {
|
||||
_counter: number;
|
||||
_initiatives: Initiative[];
|
||||
|
||||
constructor() {
|
||||
this._counter = 1;
|
||||
this._initiatives = [];
|
||||
}
|
||||
|
||||
addInitiative(shape?: $Shape<Initiative>): Initiative {
|
||||
const num = this._counter;
|
||||
this._counter++;
|
||||
|
||||
const initiative: Initiative = {
|
||||
title: `Example Initiative ${num}`,
|
||||
timestampMs: 400 + num,
|
||||
completed: false,
|
||||
tracker: topicAddress("https://example.com", num),
|
||||
dependencies: [],
|
||||
references: [],
|
||||
contributions: [],
|
||||
champions: [],
|
||||
...shape,
|
||||
};
|
||||
|
||||
this._initiatives.push(initiative);
|
||||
return initiative;
|
||||
}
|
||||
|
||||
initiatives() {
|
||||
return [...this._initiatives];
|
||||
}
|
||||
}
|
||||
|
||||
class MockReferenceDetector implements ReferenceDetector {
|
||||
_references: Map<URL, NodeAddressT>;
|
||||
|
||||
constructor() {
|
||||
this._references = new Map();
|
||||
jest.spyOn(this, "addressFromUrl");
|
||||
}
|
||||
|
||||
addReference(url: URL, address: NodeAddressT) {
|
||||
this._references.set(url, address);
|
||||
}
|
||||
|
||||
addressFromUrl(url: URL): ?NodeAddressT {
|
||||
return this._references.get(url);
|
||||
}
|
||||
}
|
||||
|
||||
function example() {
|
||||
return {
|
||||
repo: new MockInitiativeRepository(),
|
||||
refs: new MockReferenceDetector(),
|
||||
};
|
||||
}
|
||||
|
||||
function exampleNodeAddress(id: number): NodeAddressT {
|
||||
return NodeAddress.fromParts(["example", String(id)]);
|
||||
}
|
||||
|
||||
function discourseInitiativeAddress(id: number): NodeAddressT {
|
||||
return NodeAddress.append(
|
||||
initiativeNodeType.prefix,
|
||||
...NodeAddress.toParts(topicAddress("https://example.com", id))
|
||||
);
|
||||
}
|
||||
|
||||
function edgeAddress(prefix: EdgeAddressT) {
|
||||
return (
|
||||
initiativeAddress: NodeAddressT,
|
||||
other: NodeAddressT
|
||||
): EdgeAddressT => {
|
||||
return EdgeAddress.append(
|
||||
prefix,
|
||||
...NodeAddress.toParts(initiativeAddress),
|
||||
...NodeAddress.toParts(other)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const dependencyEdgeAddress = edgeAddress(dependsOnEdgeType.prefix);
|
||||
const referenceEdgeAddress = edgeAddress(referencesEdgeType.prefix);
|
||||
const contributionEdgeAddress = edgeAddress(contributesToEdgeType.prefix);
|
||||
const championEdgeAddress = edgeAddress(championsEdgeType.prefix);
|
||||
|
||||
describe("plugins/initiatives/createGraph", () => {
|
||||
describe("createGraph", () => {
|
||||
it("should add initiative nodes to the graph", () => {
|
||||
// Given
|
||||
const {repo, refs} = example();
|
||||
repo.addInitiative();
|
||||
repo.addInitiative();
|
||||
|
||||
// When
|
||||
const graph = createGraph(repo, refs);
|
||||
|
||||
// Then
|
||||
const nodes = Array.from(
|
||||
graph.nodes({prefix: initiativeNodeType.prefix})
|
||||
);
|
||||
expect(nodes).toEqual([
|
||||
{
|
||||
description: "Example Initiative 1",
|
||||
timestampMs: 401,
|
||||
address: discourseInitiativeAddress(1),
|
||||
},
|
||||
{
|
||||
description: "Example Initiative 2",
|
||||
timestampMs: 402,
|
||||
address: discourseInitiativeAddress(2),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should add the tracker as a contribution edge", () => {
|
||||
// Given
|
||||
const {repo, refs} = example();
|
||||
const i1 = repo.addInitiative();
|
||||
|
||||
// When
|
||||
const graph = createGraph(repo, refs);
|
||||
|
||||
// Then
|
||||
const contributions = Array.from(
|
||||
graph.edges({
|
||||
addressPrefix: contributesToEdgeType.prefix,
|
||||
showDangling: true,
|
||||
})
|
||||
);
|
||||
expect(contributions).toEqual([
|
||||
{
|
||||
address: contributionEdgeAddress(
|
||||
discourseInitiativeAddress(1),
|
||||
i1.tracker
|
||||
),
|
||||
dst: discourseInitiativeAddress(1),
|
||||
src: i1.tracker,
|
||||
timestampMs: i1.timestampMs,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe("reference detection attempts", () => {
|
||||
it("should attempt to resolve dependency URLs", () => {
|
||||
// Given
|
||||
const {repo, refs} = example();
|
||||
repo.addInitiative({
|
||||
dependencies: ["https://example.com/1"],
|
||||
});
|
||||
|
||||
// When
|
||||
createGraph(repo, refs);
|
||||
|
||||
// Then
|
||||
expect(refs.addressFromUrl).toHaveBeenCalledWith(
|
||||
"https://example.com/1"
|
||||
);
|
||||
});
|
||||
|
||||
it("should attempt to resolve reference URLs", () => {
|
||||
// Given
|
||||
const {repo, refs} = example();
|
||||
repo.addInitiative({
|
||||
references: ["https://example.com/2"],
|
||||
});
|
||||
|
||||
// When
|
||||
createGraph(repo, refs);
|
||||
|
||||
// Then
|
||||
expect(refs.addressFromUrl).toHaveBeenCalledWith(
|
||||
"https://example.com/2"
|
||||
);
|
||||
});
|
||||
|
||||
it("should attempt to resolve contribution URLs", () => {
|
||||
// Given
|
||||
const {repo, refs} = example();
|
||||
repo.addInitiative({
|
||||
contributions: ["https://example.com/3"],
|
||||
});
|
||||
|
||||
// When
|
||||
createGraph(repo, refs);
|
||||
|
||||
// Then
|
||||
expect(refs.addressFromUrl).toHaveBeenCalledWith(
|
||||
"https://example.com/3"
|
||||
);
|
||||
});
|
||||
|
||||
it("should attempt to resolve champion URLs", () => {
|
||||
// Given
|
||||
const {repo, refs} = example();
|
||||
repo.addInitiative({
|
||||
champions: ["https://example.com/4"],
|
||||
});
|
||||
|
||||
// When
|
||||
createGraph(repo, refs);
|
||||
|
||||
// Then
|
||||
expect(refs.addressFromUrl).toHaveBeenCalledWith(
|
||||
"https://example.com/4"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("adding detected edges", () => {
|
||||
it("should add edges for dependency URLs it can resolve", () => {
|
||||
// Given
|
||||
const {repo, refs} = example();
|
||||
refs.addReference("https://example.com/1", exampleNodeAddress(1));
|
||||
repo.addInitiative({
|
||||
dependencies: ["https://example.com/1", "https://example.com/99"],
|
||||
});
|
||||
|
||||
// When
|
||||
const graph = createGraph(repo, refs);
|
||||
|
||||
// Then
|
||||
const dependencies = Array.from(
|
||||
graph.edges({
|
||||
addressPrefix: dependsOnEdgeType.prefix,
|
||||
showDangling: true,
|
||||
})
|
||||
);
|
||||
expect(refs.addressFromUrl).toHaveBeenCalledTimes(2);
|
||||
expect(dependencies).toHaveLength(1);
|
||||
expect(dependencies).toContainEqual({
|
||||
address: dependencyEdgeAddress(
|
||||
discourseInitiativeAddress(1),
|
||||
exampleNodeAddress(1)
|
||||
),
|
||||
src: discourseInitiativeAddress(1),
|
||||
dst: exampleNodeAddress(1),
|
||||
timestampMs: 401,
|
||||
});
|
||||
});
|
||||
|
||||
it("should add edges for reference URLs it can resolve", () => {
|
||||
// Given
|
||||
const {repo, refs} = example();
|
||||
refs.addReference("https://example.com/2", exampleNodeAddress(2));
|
||||
repo.addInitiative({
|
||||
references: ["https://example.com/2", "https://example.com/99"],
|
||||
});
|
||||
|
||||
// When
|
||||
const graph = createGraph(repo, refs);
|
||||
|
||||
// Then
|
||||
const references = Array.from(
|
||||
graph.edges({
|
||||
addressPrefix: referencesEdgeType.prefix,
|
||||
showDangling: true,
|
||||
})
|
||||
);
|
||||
expect(refs.addressFromUrl).toHaveBeenCalledTimes(2);
|
||||
expect(references).toHaveLength(1);
|
||||
expect(references).toContainEqual({
|
||||
address: referenceEdgeAddress(
|
||||
discourseInitiativeAddress(1),
|
||||
exampleNodeAddress(2)
|
||||
),
|
||||
src: discourseInitiativeAddress(1),
|
||||
dst: exampleNodeAddress(2),
|
||||
timestampMs: 401,
|
||||
});
|
||||
});
|
||||
|
||||
it("should add edges for contribution URLs it can resolve", () => {
|
||||
// Given
|
||||
const {repo, refs} = example();
|
||||
refs.addReference("https://example.com/3", exampleNodeAddress(3));
|
||||
repo.addInitiative({
|
||||
contributions: ["https://example.com/3", "https://example.com/99"],
|
||||
});
|
||||
|
||||
// When
|
||||
const graph = createGraph(repo, refs);
|
||||
|
||||
// Then
|
||||
const contributions = Array.from(
|
||||
graph.edges({
|
||||
addressPrefix: contributesToEdgeType.prefix,
|
||||
showDangling: true,
|
||||
})
|
||||
);
|
||||
expect(refs.addressFromUrl).toHaveBeenCalledTimes(2);
|
||||
expect(contributions).toHaveLength(2);
|
||||
expect(contributions).toContainEqual({
|
||||
address: contributionEdgeAddress(
|
||||
discourseInitiativeAddress(1),
|
||||
exampleNodeAddress(3)
|
||||
),
|
||||
src: exampleNodeAddress(3),
|
||||
dst: discourseInitiativeAddress(1),
|
||||
timestampMs: 401,
|
||||
});
|
||||
});
|
||||
|
||||
it("should add edges for champion URLs it can resolve", () => {
|
||||
// Given
|
||||
const {repo, refs} = example();
|
||||
refs.addReference("https://example.com/4", exampleNodeAddress(4));
|
||||
repo.addInitiative({
|
||||
champions: ["https://example.com/4", "https://example.com/99"],
|
||||
});
|
||||
|
||||
// When
|
||||
const graph = createGraph(repo, refs);
|
||||
|
||||
// Then
|
||||
const champions = Array.from(
|
||||
graph.edges({
|
||||
addressPrefix: championsEdgeType.prefix,
|
||||
showDangling: true,
|
||||
})
|
||||
);
|
||||
expect(refs.addressFromUrl).toHaveBeenCalledTimes(2);
|
||||
expect(champions).toHaveLength(1);
|
||||
expect(champions).toContainEqual({
|
||||
address: championEdgeAddress(
|
||||
discourseInitiativeAddress(1),
|
||||
exampleNodeAddress(4)
|
||||
),
|
||||
src: exampleNodeAddress(4),
|
||||
dst: discourseInitiativeAddress(1),
|
||||
timestampMs: 401,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -28,3 +28,13 @@ export type Initiative = {|
|
|||
+contributions: $ReadOnlyArray<URL>,
|
||||
+champions: $ReadOnlyArray<URL>,
|
||||
|};
|
||||
|
||||
/**
|
||||
* Represents a source of Initiatives.
|
||||
*/
|
||||
export interface InitiativeRepository {
|
||||
/**
|
||||
* Gets an array of all Initiatives in this repository.
|
||||
*/
|
||||
initiatives(): $ReadOnlyArray<Initiative>;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue