Initiatives: implement basic createGraph (#1477)

This commit is contained in:
Robin van Boven 2020-01-07 14:07:27 +01:00 committed by GitHub
parent 579b01ed46
commit b05cc84f2e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 465 additions and 0 deletions

View File

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

View File

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

View File

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