From 22ca77ed05468194681381d90b1bc35a12996a45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dandelion=20Man=C3=A9?= Date: Mon, 30 Apr 2018 10:07:23 -0700 Subject: [PATCH] Add safe type coercion for GitHub api (#173) In general, methods in the porcelain GitHub api may return multiple types; e.g. a reference could be to an Issue, PullRequest, Comment, Author (or more). To make working with the api more convenient while maintaining safety, this commit adds a static `asType` method to each Entity class, which confirms that type coercion is safe, and errors if not. This commit also adds `issueOrPRByNumber`, a convenience method, to api.test.js. Test plan: Check the API usage and verify that it is reasonable. --- src/plugins/github/api.js | 31 ++++++++++++++++++++++- src/plugins/github/api.test.js | 45 +++++++++++++++++++++++++--------- 2 files changed, 63 insertions(+), 13 deletions(-) diff --git a/src/plugins/github/api.js b/src/plugins/github/api.js index 9489cb5..a518a3c 100644 --- a/src/plugins/github/api.js +++ b/src/plugins/github/api.js @@ -1,5 +1,7 @@ // @flow +import stringify from "json-stable-stringify"; + import {Graph} from "../../core/graph"; import type {Node} from "../../core/graph"; import type {Address} from "../../core/address"; @@ -23,6 +25,16 @@ import { PULL_REQUEST_NODE_TYPE, } from "./types"; +export type Entity = Issue | PullRequest | Comment | Author; + +function assertEntityType(e: Entity, t: NodeType) { + if (e.type() !== t) { + throw new Error( + `Expected entity at ${stringify(e.address())} to have type ${t}` + ); + } +} + export class Repository { repositoryName: string; graph: Graph; @@ -123,6 +135,10 @@ class Commentable extends Post< } export class Author extends GithubEntity { + static from(e: Entity): Author { + assertEntityType(e, AUTHOR_NODE_TYPE); + return (e: any); + } login(): string { return this.node().payload.login; } @@ -133,6 +149,10 @@ export class Author extends GithubEntity { } export class PullRequest extends Commentable { + static from(e: Entity): PullRequest { + assertEntityType(e, PULL_REQUEST_NODE_TYPE); + return (e: any); + } number(): number { return this.node().payload.number; } @@ -142,6 +162,10 @@ export class PullRequest extends Commentable { } export class Issue extends Commentable { + static from(e: Entity): Issue { + assertEntityType(e, ISSUE_NODE_TYPE); + return (e: any); + } number(): number { return this.node().payload.number; } @@ -150,4 +174,9 @@ export class Issue extends Commentable { } } -export class Comment extends Post {} +export class Comment extends Post { + static from(e: Entity): Comment { + assertEntityType(e, COMMENT_NODE_TYPE); + return (e: any); + } +} diff --git a/src/plugins/github/api.test.js b/src/plugins/github/api.test.js index f12ddbe..7aa17db 100644 --- a/src/plugins/github/api.test.js +++ b/src/plugins/github/api.test.js @@ -12,12 +12,16 @@ import { describe("GitHub porcelain API", () => { const graph = parse("sourcecred/example-repo", exampleRepoData); const repo = new Repository("sourcecred/example-repo", graph); + function issueOrPRByNumber(n: number): Issue | PullRequest { + const result = repo.issueOrPRByNumber(n); + if (result == null) { + throw new Error(`Expected Issue/PR ${n} to exist`); + } + return result; + } it("Issue", () => { - const issue = repo.issueOrPRByNumber(1); - if (issue == null) { - throw new Error("Issue reaching issue!"); - } + const issue = issueOrPRByNumber(1); expect(issue.title()).toBe("An example issue."); expect(issue.body()).toBe("This is just an example issue."); expect(issue.number()).toBe(1); @@ -31,10 +35,7 @@ describe("GitHub porcelain API", () => { }); it("PullRequest", () => { - const pullRequest = repo.issueOrPRByNumber(3); - if (pullRequest == null) { - throw new Error("Issue reaching PR!"); - } + const pullRequest = issueOrPRByNumber(3); expect(pullRequest.body()).toBe("Oh look, it's a pull request."); expect(pullRequest.url()).toBe( "https://github.com/sourcecred/example-repo/pull/3" @@ -46,10 +47,7 @@ describe("GitHub porcelain API", () => { }); it("Comment", () => { - const issue = repo.issueOrPRByNumber(6); - if (issue == null) { - throw new Error("Issue reaching issue!"); - } + const issue = issueOrPRByNumber(6); const comments = issue.comments(); expect(comments.length).toMatchSnapshot(); const comment = comments[0]; @@ -78,4 +76,27 @@ describe("GitHub porcelain API", () => { expect(decentralion.node()).toMatchSnapshot(); expect(decentralion.address()).toEqual(decentralion.node().address); }); + + describe("type coercion", () => { + it("type coercion works when typed correctly", () => { + const issue: Issue = Issue.from(issueOrPRByNumber(1)); + const pr: PullRequest = PullRequest.from(issueOrPRByNumber(3)); + const author: Author = Author.from(issueOrPRByNumber(3).authors()[0]); + const comment: Comment = Comment.from(issueOrPRByNumber(2).comments()[0]); + }); + it("type coercion throws error when typed incorrectly", () => { + expect(() => PullRequest.from(issueOrPRByNumber(1))).toThrowError( + "to have type PULL_REQUEST" + ); + expect(() => Issue.from(issueOrPRByNumber(3))).toThrowError( + "to have type ISSUE" + ); + expect(() => + Comment.from(issueOrPRByNumber(3).authors()[0]) + ).toThrowError("to have type COMMENT"); + expect(() => + Author.from(issueOrPRByNumber(2).comments()[0]) + ).toThrowError("to have type AUTHOR"); + }); + }); });