Add GitHub `edges` module (#385)

Summary:
This module includes a raw edge type, a structured edge type, and edge
creation functions that take source and destination and create an edge.

Test Plan:
Unit tests added. These cover all of the successful cases, and none of
the unsuccessful cases. We plan to refactor this code Soon™, and it is
hard to see how to nicely factor the tests without just testing the same
code paths over and over.

wchargin-branch: github-edges
This commit is contained in:
William Chargin 2018-06-13 16:19:50 -07:00 committed by GitHub
parent 17b390afe9
commit 2491fcd3cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 477 additions and 0 deletions

View File

@ -0,0 +1,149 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`plugins/github/edges createEdge works for authors edges 1`] = `"{address: EdgeAddress[\\"sourcecred\\",\\"github\\",\\"authors\\",\\"2\\",\\"userlike\\",\\"decentralion\\",\\"4\\",\\"issue\\",\\"sourcecred\\",\\"example-github\\",\\"2\\"], src: NodeAddress[\\"sourcecred\\",\\"github\\",\\"userlike\\",\\"decentralion\\"], dst: NodeAddress[\\"sourcecred\\",\\"github\\",\\"issue\\",\\"sourcecred\\",\\"example-github\\",\\"2\\"]}"`;
exports[`plugins/github/edges createEdge works for has-parent edges 1`] = `"{address: EdgeAddress[\\"sourcecred\\",\\"github\\",\\"has_parent\\",\\"7\\",\\"comment\\",\\"review\\",\\"sourcecred\\",\\"example-github\\",\\"5\\",\\"100313899\\",\\"171460198\\"], src: NodeAddress[\\"sourcecred\\",\\"github\\",\\"comment\\",\\"review\\",\\"sourcecred\\",\\"example-github\\",\\"5\\",\\"100313899\\",\\"171460198\\"], dst: NodeAddress[\\"sourcecred\\",\\"github\\",\\"review\\",\\"sourcecred\\",\\"example-github\\",\\"5\\",\\"100313899\\"]}"`;
exports[`plugins/github/edges createEdge works for merged-as edges 1`] = `"{address: EdgeAddress[\\"sourcecred\\",\\"github\\",\\"merged_as\\",\\"4\\",\\"pull\\",\\"sourcecred\\",\\"example-github\\",\\"5\\"], src: NodeAddress[\\"sourcecred\\",\\"github\\",\\"pull\\",\\"sourcecred\\",\\"example-github\\",\\"5\\"], dst: NodeAddress[\\"git\\",\\"commit\\",\\"123\\"]}"`;
exports[`plugins/github/edges createEdge works for reference edges 1`] = `"{address: EdgeAddress[\\"sourcecred\\",\\"github\\",\\"references\\",\\"4\\",\\"issue\\",\\"sourcecred\\",\\"example-github\\",\\"2\\",\\"4\\",\\"pull\\",\\"sourcecred\\",\\"example-github\\",\\"5\\"], src: NodeAddress[\\"sourcecred\\",\\"github\\",\\"issue\\",\\"sourcecred\\",\\"example-github\\",\\"2\\"], dst: NodeAddress[\\"sourcecred\\",\\"github\\",\\"pull\\",\\"sourcecred\\",\\"example-github\\",\\"5\\"]}"`;
exports[`plugins/github/edges snapshots as expected: authors 1`] = `
Object {
"address": Array [
"sourcecred",
"github",
"authors",
"2",
"userlike",
"decentralion",
"4",
"pull",
"sourcecred",
"example-github",
"5",
],
"structured": Object {
"author": Object {
"login": "decentralion",
"type": "USERLIKE",
},
"content": Object {
"number": "5",
"repo": Object {
"name": "example-github",
"owner": "sourcecred",
"type": "REPO",
},
"type": "PULL",
},
"type": "AUTHORS",
},
}
`;
exports[`plugins/github/edges snapshots as expected: hasParent 1`] = `
Object {
"address": Array [
"sourcecred",
"github",
"has_parent",
"7",
"comment",
"review",
"sourcecred",
"example-github",
"5",
"100313899",
"171460198",
],
"structured": Object {
"child": Object {
"id": "171460198",
"parent": Object {
"id": "100313899",
"pull": Object {
"number": "5",
"repo": Object {
"name": "example-github",
"owner": "sourcecred",
"type": "REPO",
},
"type": "PULL",
},
"type": "REVIEW",
},
"type": "COMMENT",
},
"type": "HAS_PARENT",
},
}
`;
exports[`plugins/github/edges snapshots as expected: mergedAs 1`] = `
Object {
"address": Array [
"sourcecred",
"github",
"merged_as",
"4",
"pull",
"sourcecred",
"example-github",
"5",
],
"structured": Object {
"pull": Object {
"number": "5",
"repo": Object {
"name": "example-github",
"owner": "sourcecred",
"type": "REPO",
},
"type": "PULL",
},
"type": "MERGED_AS",
},
}
`;
exports[`plugins/github/edges snapshots as expected: references 1`] = `
Object {
"address": Array [
"sourcecred",
"github",
"references",
"4",
"issue",
"sourcecred",
"example-github",
"2",
"4",
"issue",
"sourcecred",
"example-github",
"1",
],
"structured": Object {
"referent": Object {
"number": "1",
"repo": Object {
"name": "example-github",
"owner": "sourcecred",
"type": "REPO",
},
"type": "ISSUE",
},
"referrer": Object {
"number": "2",
"repo": Object {
"name": "example-github",
"owner": "sourcecred",
"type": "REPO",
},
"type": "ISSUE",
},
"type": "REFERENCES",
},
}
`;

View File

@ -0,0 +1,208 @@
// @flow
import {
type Edge,
type EdgeAddressT,
type NodeAddressT,
EdgeAddress,
NodeAddress,
} from "../../core/graph";
import * as GithubNode from "./nodes";
export opaque type RawAddress: EdgeAddressT = EdgeAddressT;
export type AuthorsAddress = {|
+type: "AUTHORS",
+author: GithubNode.UserlikeAddress,
+content: GithubNode.AuthorableAddress,
|};
export type MergedAsAddress = {|
+type: "MERGED_AS",
+pull: GithubNode.PullAddress,
|};
export type HasParentAddress = {|
+type: "HAS_PARENT",
+child: GithubNode.ChildAddress,
|};
export type ReferencesAddress = {|
+type: "REFERENCES",
+referrer: GithubNode.TextContentAddress,
+referent: GithubNode.ReferentAddress,
|};
export type StructuredAddress =
| AuthorsAddress
| MergedAsAddress
| HasParentAddress
| ReferencesAddress;
export const createEdge = Object.freeze({
authors: (
author: GithubNode.UserlikeAddress,
content: GithubNode.AuthorableAddress
): Edge => ({
address: toRaw({type: "AUTHORS", author, content}),
src: GithubNode.toRaw(author),
dst: GithubNode.toRaw(content),
}),
mergedAs: (
pull: GithubNode.PullAddress,
commitAddress: NodeAddressT /* TODO: Make this a Git commit node address. */
): Edge => ({
address: toRaw({type: "MERGED_AS", pull}),
src: GithubNode.toRaw(pull),
dst: commitAddress,
}),
hasParent: (
child: GithubNode.ChildAddress,
parent: GithubNode.ParentAddress
): Edge => ({
address: toRaw({type: "HAS_PARENT", child}),
src: GithubNode.toRaw(child),
dst: GithubNode.toRaw(parent),
}),
references: (
referrer: GithubNode.TextContentAddress,
referent: GithubNode.ReferentAddress
): Edge => ({
address: toRaw({type: "REFERENCES", referrer, referent}),
src: GithubNode.toRaw(referrer),
dst: GithubNode.toRaw(referent),
}),
});
const NODE_PREFIX_LENGTH = NodeAddress.toParts(GithubNode._githubAddress())
.length;
const GITHUB_PREFIX = EdgeAddress.fromParts(["sourcecred", "github"]);
function githubEdgeAddress(...parts: string[]): RawAddress {
return EdgeAddress.append(GITHUB_PREFIX, ...parts);
}
function lengthEncode(x: GithubNode.RawAddress): $ReadOnlyArray<string> {
const baseParts = NodeAddress.toParts(x).slice(NODE_PREFIX_LENGTH);
return [String(baseParts.length), ...baseParts];
}
function lengthDecode(
x: $ReadOnlyArray<string>,
fail: () => Error
): {|+parts: $ReadOnlyArray<string>, +rest: $ReadOnlyArray<string>|} {
if (x.length === 0) {
// Not length-encoded.
throw fail();
}
const [lengthString, ...allParts] = x;
const length = parseInt(lengthString, 10);
if (isNaN(length)) {
throw fail();
}
if (length > allParts.length) {
// Not enough elements.
throw fail();
}
return {parts: allParts.slice(0, length), rest: allParts.slice(length)};
}
function multiLengthDecode(x: $ReadOnlyArray<string>, fail: () => Error) {
let remaining = x;
let partses = [];
while (remaining.length > 0) {
const {parts, rest} = lengthDecode(remaining, fail);
partses.push(parts);
remaining = rest;
}
return partses;
}
export function fromRaw(x: RawAddress): StructuredAddress {
function fail() {
return new Error(`Bad address: ${EdgeAddress.toString(x)}`);
}
if (!EdgeAddress.hasPrefix(x, GITHUB_PREFIX)) {
throw fail();
}
const [_unused_sc, _unused_gh, kind, ...rest] = EdgeAddress.toParts(x);
switch (kind) {
case "authors": {
const parts = multiLengthDecode(rest, fail);
if (parts.length !== 2) {
throw fail();
}
const [authorParts, contentParts] = parts;
const author: GithubNode.UserlikeAddress = (GithubNode.fromRaw(
GithubNode._githubAddress(...authorParts)
): any);
const content: GithubNode.AuthorableAddress = (GithubNode.fromRaw(
GithubNode._githubAddress(...contentParts)
): any);
return ({type: "AUTHORS", author, content}: AuthorsAddress);
}
case "merged_as": {
const parts = multiLengthDecode(rest, fail);
if (parts.length !== 1) {
throw fail();
}
const [pullParts] = parts;
const pull: GithubNode.PullAddress = (GithubNode.fromRaw(
GithubNode._githubAddress(...pullParts)
): any);
return ({type: "MERGED_AS", pull}: MergedAsAddress);
}
case "has_parent": {
const parts = multiLengthDecode(rest, fail);
if (parts.length !== 1) {
throw fail();
}
const [childParts] = parts;
const child: GithubNode.ChildAddress = (GithubNode.fromRaw(
GithubNode._githubAddress(...childParts)
): any);
return ({type: "HAS_PARENT", child}: HasParentAddress);
}
case "references": {
const parts = multiLengthDecode(rest, fail);
if (parts.length !== 2) {
throw fail();
}
const [referrerParts, referentParts] = parts;
const referrer: GithubNode.TextContentAddress = (GithubNode.fromRaw(
GithubNode._githubAddress(...referrerParts)
): any);
const referent: GithubNode.ReferentAddress = (GithubNode.fromRaw(
GithubNode._githubAddress(...referentParts)
): any);
return ({type: "REFERENCES", referrer, referent}: ReferencesAddress);
}
default:
throw fail();
}
}
export function toRaw(x: StructuredAddress): RawAddress {
switch (x.type) {
case "AUTHORS":
return githubEdgeAddress(
"authors",
...lengthEncode(GithubNode.toRaw(x.author)),
...lengthEncode(GithubNode.toRaw(x.content))
);
case "MERGED_AS":
return githubEdgeAddress(
"merged_as",
...lengthEncode(GithubNode.toRaw(x.pull))
);
case "HAS_PARENT":
return githubEdgeAddress(
"has_parent",
...lengthEncode(GithubNode.toRaw(x.child))
);
case "REFERENCES":
return githubEdgeAddress(
"references",
...lengthEncode(GithubNode.toRaw(x.referrer)),
...lengthEncode(GithubNode.toRaw(x.referent))
);
default:
// eslint-disable-next-line no-unused-expressions
(x.type: empty);
throw new Error(x.type);
}
}

View File

@ -0,0 +1,120 @@
// @flow
import {NodeAddress, EdgeAddress, edgeToString} from "../../core/graph";
import {createEdge, fromRaw, toRaw} from "./edges";
describe("plugins/github/edges", () => {
const nodeExamples = {
repo: () => ({
type: "REPO",
owner: "sourcecred",
name: "example-github",
}),
issue: () => ({type: "ISSUE", repo: nodeExamples.repo(), number: "2"}),
pull: () => ({type: "PULL", repo: nodeExamples.repo(), number: "5"}),
review: () => ({
type: "REVIEW",
pull: nodeExamples.pull(),
id: "100313899",
}),
issueComment: () => ({
type: "COMMENT",
parent: nodeExamples.issue(),
id: "373768703",
}),
pullComment: () => ({
type: "COMMENT",
parent: nodeExamples.pull(),
id: "396430464",
}),
reviewComment: () => ({
type: "COMMENT",
parent: nodeExamples.review(),
id: "171460198",
}),
user: () => ({type: "USERLIKE", login: "decentralion"}),
};
const edgeExamples = {
authors: () => ({
type: "AUTHORS",
author: nodeExamples.user(),
content: nodeExamples.pull(),
}),
mergedAs: () => ({
type: "MERGED_AS",
pull: nodeExamples.pull(),
}),
hasParent: () => ({
type: "HAS_PARENT",
child: nodeExamples.reviewComment(),
}),
references: () => ({
type: "REFERENCES",
referrer: nodeExamples.issue(),
referent: {type: "ISSUE", repo: nodeExamples.repo(), number: "1"},
}),
};
describe("createEdge", () => {
it("works for authors edges", () => {
expect(
edgeToString(
createEdge.authors(nodeExamples.user(), nodeExamples.issue())
)
).toMatchSnapshot();
});
it("works for merged-as edges", () => {
const commitAddress = NodeAddress.fromParts(["git", "commit", "123"]);
expect(
edgeToString(createEdge.mergedAs(nodeExamples.pull(), commitAddress))
).toMatchSnapshot();
});
it("works for has-parent edges", () => {
expect(
edgeToString(
createEdge.hasParent(
nodeExamples.reviewComment(),
nodeExamples.review()
)
)
).toMatchSnapshot();
});
it("works for reference edges", () => {
expect(
edgeToString(
createEdge.references(nodeExamples.issue(), nodeExamples.pull())
)
).toMatchSnapshot();
});
});
describe("`fromRaw` after `toRaw` is identity", () => {
Object.keys(edgeExamples).forEach((example) => {
it(example, () => {
const instance = edgeExamples[example]();
expect(fromRaw(toRaw(instance))).toEqual(instance);
});
});
});
describe("`toRaw` after `fromRaw` is identity", () => {
Object.keys(edgeExamples).forEach((example) => {
it(example, () => {
const instance = edgeExamples[example]();
const raw = toRaw(instance);
expect(toRaw(fromRaw(raw))).toEqual(raw);
});
});
});
describe("snapshots as expected:", () => {
Object.keys(edgeExamples).forEach((example) => {
it(example, () => {
const instance = edgeExamples[example]();
const raw = EdgeAddress.toParts(toRaw(instance));
expect({address: raw, structured: instance}).toMatchSnapshot();
});
});
});
});