Add an address module for the GitHub plugin (#380)

Summary:
This module exposes a structured type `StructuredAddress`, an embedding
`GithubAddressT` of this type into the `NodeAddress` layer, and
functions to convert between the two.

Paired with @wchargin.

Test Plan:
Unit tests added, with full coverage. Snapshots are easily readable.
This commit is contained in:
Dandelion Mané 2018-06-12 11:05:09 -07:00 committed by GitHub
parent 0339d9f41b
commit 773596755a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 599 additions and 0 deletions

View File

@ -0,0 +1,193 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`plugins/github/address snapshots as expected: issue 1`] = `
Object {
"address": Array [
"sourcecred",
"github",
"issue",
"sourcecred",
"example-github",
"2",
],
"structured": Object {
"number": "2",
"repo": Object {
"name": "example-github",
"owner": "sourcecred",
"type": "REPO",
},
"type": "ISSUE",
},
}
`;
exports[`plugins/github/address snapshots as expected: issueComment 1`] = `
Object {
"address": Array [
"sourcecred",
"github",
"comment",
"issue",
"sourcecred",
"example-github",
"2",
"373768703",
],
"structured": Object {
"fragment": "373768703",
"parent": Object {
"number": "2",
"repo": Object {
"name": "example-github",
"owner": "sourcecred",
"type": "REPO",
},
"type": "ISSUE",
},
"type": "COMMENT",
},
}
`;
exports[`plugins/github/address snapshots as expected: pull 1`] = `
Object {
"address": Array [
"sourcecred",
"github",
"pull",
"sourcecred",
"example-github",
"5",
],
"structured": Object {
"number": "5",
"repo": Object {
"name": "example-github",
"owner": "sourcecred",
"type": "REPO",
},
"type": "PULL",
},
}
`;
exports[`plugins/github/address snapshots as expected: pullComment 1`] = `
Object {
"address": Array [
"sourcecred",
"github",
"comment",
"pull",
"sourcecred",
"example-github",
"5",
"396430464",
],
"structured": Object {
"fragment": "396430464",
"parent": Object {
"number": "5",
"repo": Object {
"name": "example-github",
"owner": "sourcecred",
"type": "REPO",
},
"type": "PULL",
},
"type": "COMMENT",
},
}
`;
exports[`plugins/github/address snapshots as expected: repo 1`] = `
Object {
"address": Array [
"sourcecred",
"github",
"repo",
"sourcecred",
"example-github",
],
"structured": Object {
"name": "example-github",
"owner": "sourcecred",
"type": "REPO",
},
}
`;
exports[`plugins/github/address snapshots as expected: review 1`] = `
Object {
"address": Array [
"sourcecred",
"github",
"review",
"sourcecred",
"example-github",
"5",
"100313899",
],
"structured": Object {
"fragment": "100313899",
"pull": Object {
"number": "5",
"repo": Object {
"name": "example-github",
"owner": "sourcecred",
"type": "REPO",
},
"type": "PULL",
},
"type": "REVIEW",
},
}
`;
exports[`plugins/github/address snapshots as expected: reviewComment 1`] = `
Object {
"address": Array [
"sourcecred",
"github",
"comment",
"review",
"sourcecred",
"example-github",
"5",
"100313899",
"171460198",
],
"structured": Object {
"fragment": "171460198",
"parent": Object {
"fragment": "100313899",
"pull": Object {
"number": "5",
"repo": Object {
"name": "example-github",
"owner": "sourcecred",
"type": "REPO",
},
"type": "PULL",
},
"type": "REVIEW",
},
"type": "COMMENT",
},
}
`;
exports[`plugins/github/address snapshots as expected: user 1`] = `
Object {
"address": Array [
"sourcecred",
"github",
"userlike",
"decentralion",
],
"structured": Object {
"login": "decentralion",
"type": "USERLIKE",
},
}
`;

View File

@ -0,0 +1,199 @@
// @flow
import {NodeAddress, type NodeAddressT} from "../../core/graph";
export opaque type GithubAddressT: NodeAddressT = NodeAddressT;
const GITHUB_PREFIX = NodeAddress.fromParts(["sourcecred", "github"]);
function githubAddress(...parts: string[]): GithubAddressT {
return NodeAddress.append(GITHUB_PREFIX, ...parts);
}
export type RepoAddress = {|
+type: "REPO",
+owner: string,
+name: string,
|};
export type IssueAddress = {|
+type: "ISSUE",
+repo: RepoAddress,
+number: string,
|};
export type PullAddress = {|
+type: "PULL",
+repo: RepoAddress,
+number: string,
|};
export type ReviewAddress = {|
+type: "REVIEW",
+pull: PullAddress,
+fragment: string,
|};
export type CommentAddress = {|
+type: "COMMENT",
+parent: IssueAddress | PullAddress | ReviewAddress,
+fragment: string,
|};
export type UserlikeAddress = {|
+type: "USERLIKE",
+login: string,
|};
export type StructuredAddress =
| RepoAddress
| IssueAddress
| PullAddress
| ReviewAddress
| CommentAddress
| UserlikeAddress;
export function structure(x: GithubAddressT): StructuredAddress {
function fail() {
return new Error(`Bad address: ${NodeAddress.toString(x)}`);
}
if (!NodeAddress.hasPrefix(x, GITHUB_PREFIX)) {
throw fail();
}
const [_unused_sc, _unused_gh, kind, ...rest] = NodeAddress.toParts(x);
switch (kind) {
case "repo": {
if (rest.length !== 2) {
throw fail();
}
const [owner, name] = rest;
return {type: "REPO", owner, name};
}
case "issue": {
if (rest.length !== 3) {
throw fail();
}
const [owner, name, number] = rest;
const repo = {type: "REPO", owner, name};
return {type: "ISSUE", repo, number};
}
case "pull": {
if (rest.length !== 3) {
throw fail();
}
const [owner, name, number] = rest;
const repo = {type: "REPO", owner, name};
return {type: "PULL", repo, number};
}
case "review": {
if (rest.length !== 4) {
throw fail();
}
const [owner, name, pullNumber, fragment] = rest;
const repo = {type: "REPO", owner, name};
const pull = {type: "PULL", repo, number: pullNumber};
return {type: "REVIEW", pull, fragment};
}
case "comment": {
if (rest.length < 1) {
throw fail();
}
const [subkind, ...subrest] = rest;
switch (subkind) {
case "issue": {
if (subrest.length !== 4) {
throw fail();
}
const [owner, name, issueNumber, fragment] = subrest;
const repo = {type: "REPO", owner, name};
const issue = {type: "ISSUE", repo, number: issueNumber};
return {type: "COMMENT", parent: issue, fragment};
}
case "pull": {
if (subrest.length !== 4) {
throw fail();
}
const [owner, name, pullNumber, fragment] = subrest;
const repo = {type: "REPO", owner, name};
const pull = {type: "PULL", repo, number: pullNumber};
return {type: "COMMENT", parent: pull, fragment};
}
case "review": {
if (subrest.length !== 5) {
throw fail();
}
const [owner, name, pullNumber, reviewFragment, fragment] = subrest;
const repo = {type: "REPO", owner, name};
const pull = {type: "PULL", repo, number: pullNumber};
const review = {type: "REVIEW", pull, fragment: reviewFragment};
return {type: "COMMENT", parent: review, fragment};
}
default:
throw fail();
}
}
case "userlike": {
if (rest.length !== 1) {
throw fail();
}
const [login] = rest;
return {type: "USERLIKE", login};
}
default:
throw fail();
}
}
export function destructure(x: StructuredAddress): GithubAddressT {
switch (x.type) {
case "REPO":
return githubAddress("repo", x.owner, x.name);
case "ISSUE":
return githubAddress("issue", x.repo.owner, x.repo.name, x.number);
case "PULL":
return githubAddress("pull", x.repo.owner, x.repo.name, x.number);
case "REVIEW":
return githubAddress(
"review",
x.pull.repo.owner,
x.pull.repo.name,
x.pull.number,
x.fragment
);
case "COMMENT":
switch (x.parent.type) {
case "ISSUE":
return githubAddress(
"comment",
"issue",
x.parent.repo.owner,
x.parent.repo.name,
x.parent.number,
x.fragment
);
case "PULL":
return githubAddress(
"comment",
"pull",
x.parent.repo.owner,
x.parent.repo.name,
x.parent.number,
x.fragment
);
case "REVIEW":
return githubAddress(
"comment",
"review",
x.parent.pull.repo.owner,
x.parent.pull.repo.name,
x.parent.pull.number,
x.parent.fragment,
x.fragment
);
default:
// eslint-disable-next-line no-unused-expressions
(x.parent.type: empty);
throw new Error(`Bad comment parent type: ${x.parent.type}`);
}
case "USERLIKE":
return githubAddress("userlike", x.login);
default:
// eslint-disable-next-line no-unused-expressions
(x.type: empty);
throw new Error(`Unexpected type ${x.type}`);
}
}

View File

@ -0,0 +1,207 @@
// @flow
import {structure, destructure} from "./address";
import {NodeAddress} from "../../core/graph";
describe("plugins/github/address", () => {
const repo = () => ({
type: "REPO",
owner: "sourcecred",
name: "example-github",
});
const issue = () => ({type: "ISSUE", repo: repo(), number: "2"});
const pull = () => ({type: "PULL", repo: repo(), number: "5"});
const review = () => ({type: "REVIEW", pull: pull(), fragment: "100313899"});
const issueComment = () => ({
type: "COMMENT",
parent: issue(),
fragment: "373768703",
});
const pullComment = () => ({
type: "COMMENT",
parent: pull(),
fragment: "396430464",
});
const reviewComment = () => ({
type: "COMMENT",
parent: review(),
fragment: "171460198",
});
const user = () => ({type: "USERLIKE", login: "decentralion"});
const examples = {
repo,
issue,
pull,
review,
issueComment,
pullComment,
reviewComment,
user,
};
describe("Structured -> Raw -> Structured is identity", () => {
Object.keys(examples).forEach((example) => {
it(example, () => {
const instance = examples[example]();
expect(structure(destructure(instance))).toEqual(instance);
});
});
});
describe("Raw -> Structured -> Raw is identity", () => {
Object.keys(examples).forEach((example) => {
it(example, () => {
const instance = examples[example]();
const raw = destructure(instance);
expect(destructure(structure(raw))).toEqual(raw);
});
});
});
describe("snapshots as expected:", () => {
Object.keys(examples).forEach((example) => {
it(example, () => {
const instance = examples[example]();
const raw = NodeAddress.toParts(destructure(instance));
expect({address: raw, structured: instance}).toMatchSnapshot();
});
});
});
describe("errors on", () => {
describe("structure(...) with", () => {
function expectBadAddress(name: string, parts: $ReadOnlyArray<string>) {
it(name, () => {
const address = NodeAddress.fromParts([
"sourcecred",
"github",
...parts,
]);
// $ExpectFlowError
expect(() => structure(address)).toThrow("Bad address");
});
}
function checkBadCases(
partses: $ReadOnlyArray<{|
+name: string,
+parts: $ReadOnlyArray<string>,
|}>
) {
let partsAccumulator = [];
for (const {name, parts} of partses) {
const theseParts = [...partsAccumulator, ...parts];
expectBadAddress(name, theseParts);
partsAccumulator = theseParts;
}
}
it("undefined", () => {
// $ExpectFlowError
expect(() => structure(undefined)).toThrow("undefined");
});
it("null", () => {
// $ExpectFlowError
expect(() => structure(null)).toThrow("null");
});
it("with bad prefix", () => {
// $ExpectFlowError
expect(() => structure(NodeAddress.fromParts(["foo"]))).toThrow(
"Bad address"
);
});
expectBadAddress("no kind", []);
describe("repository with", () => {
checkBadCases([
{name: "no owner", parts: ["repo"]},
{name: "no name", parts: ["owner"]},
{name: "extra parts", parts: ["name", "foo"]},
]);
});
describe("issue with", () => {
checkBadCases([
{name: "no owner", parts: ["issue"]},
{name: "no name", parts: ["owner"]},
{name: "no number", parts: ["name"]},
{name: "extra parts", parts: ["123", "foo"]},
]);
});
describe("pull request with", () => {
checkBadCases([
{name: "no owner", parts: ["pull"]},
{name: "no name", parts: ["owner"]},
{name: "no number", parts: ["name"]},
{name: "extra parts", parts: ["123", "foo"]},
]);
});
describe("pull request review with", () => {
checkBadCases([
{name: "no owner", parts: ["review"]},
{name: "no name", parts: ["owner"]},
{name: "no number", parts: ["name"]},
{name: "no fragment", parts: ["123"]},
{name: "extra parts", parts: ["987", "foo"]},
]);
});
describe("comment", () => {
expectBadAddress("with no subkind", ["comment"]);
expectBadAddress("with bad subkind", ["comment", "icecream"]);
describe("on issue with", () => {
checkBadCases([
{name: "no owner", parts: ["comment", "issue"]},
{name: "no name", parts: ["owner"]},
{name: "no number", parts: ["name"]},
{name: "no fragment", parts: ["123"]},
{name: "extra parts", parts: ["987", "foo"]},
]);
});
describe("on pull request with", () => {
checkBadCases([
{name: "no owner", parts: ["comment", "pull"]},
{name: "no name", parts: ["owner"]},
{name: "no number", parts: ["name"]},
{name: "no fragment", parts: ["123"]},
{name: "extra parts", parts: ["987", "foo"]},
]);
});
describe("on pull request review with", () => {
checkBadCases([
{name: "no owner", parts: ["comment", "review"]},
{name: "no name", parts: ["owner"]},
{name: "no number", parts: ["name"]},
{name: "no review fragment", parts: ["123"]},
{name: "no comment fragment", parts: ["987"]},
{name: "extra parts", parts: ["654", "foo"]},
]);
});
});
describe("userlike", () => {
checkBadCases([
{name: "no login", parts: ["userlike"]},
{name: "extra parts", parts: ["decentra", "lion"]},
]);
});
});
describe("destructure(...) with", () => {
it("null", () => {
// $ExpectFlowError
expect(() => destructure(null)).toThrow("null");
});
it("undefined", () => {
// $ExpectFlowError
expect(() => destructure(undefined)).toThrow("undefined");
});
it("bad type", () => {
// $ExpectFlowError
expect(() => destructure({type: "ICE_CREAM"})).toThrow(
"Unexpected type"
);
});
it("bad comment type", () => {
expect(() => {
// $ExpectFlowError
destructure({type: "COMMENT", parent: {type: "ICE_CREAM"}});
}).toThrow("Bad comment parent type");
});
});
});
});