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:
parent
17b390afe9
commit
2491fcd3cb
|
@ -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",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue