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:
parent
0339d9f41b
commit
773596755a
|
@ -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",
|
||||
},
|
||||
}
|
||||
`;
|
|
@ -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}`);
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue