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