diff --git a/src/app/credExplorer/pagerankTable.js b/src/app/credExplorer/pagerankTable.js index 8995c17..d344278 100644 --- a/src/app/credExplorer/pagerankTable.js +++ b/src/app/credExplorer/pagerankTable.js @@ -6,6 +6,7 @@ import stringify from "json-stable-stringify"; import {Graph} from "../../core/graph"; import type {Address} from "../../core/address"; import {AddressMap} from "../../core/address"; +import {NodeReference} from "../../core/porcelain"; import {PLUGIN_NAME as GITHUB_PLUGIN_NAME} from "../../plugins/github/pluginName"; import {GIT_PLUGIN_NAME} from "../../plugins/git/types"; import {nodeDescription as githubNodeDescription} from "../../plugins/github/render"; @@ -24,16 +25,16 @@ type State = { |}, }; -function nodeDescription(graph, address) { - switch (address.pluginName) { +function nodeDescription(ref) { + switch (ref.address().pluginName) { case GITHUB_PLUGIN_NAME: { - return githubNodeDescription(graph, address); + return githubNodeDescription(ref); } case GIT_PLUGIN_NAME: { - return gitNodeDescription(graph, address); + return gitNodeDescription(ref.graph(), ref.address()); } default: { - return stringify(address); + return stringify(ref.address()); } } } @@ -184,7 +185,7 @@ class RecursiveTable extends React.Component { > {expanded ? "\u2212" : "+"} - {nodeDescription(graph, address)} + {nodeDescription(new NodeReference(graph, address))} {(score * 100).toPrecision(3)} {Math.log(score).toPrecision(3)} diff --git a/src/plugins/github/porcelain.js b/src/plugins/github/porcelain.js index 67c32c8..40e5d2a 100644 --- a/src/plugins/github/porcelain.js +++ b/src/plugins/github/porcelain.js @@ -15,9 +15,9 @@ */ import stringify from "json-stable-stringify"; -import {Graph} from "../../core/graph"; -import type {Node} from "../../core/graph"; import type {Address} from "../../core/address"; +import {Graph} from "../../core/graph"; +import {NodeReference, NodePorcelain} from "../../core/porcelain"; import type { AuthorNodePayload, AuthorSubtype, @@ -44,36 +44,26 @@ import { PULL_REQUEST_NODE_TYPE, PULL_REQUEST_REVIEW_COMMENT_NODE_TYPE, PULL_REQUEST_REVIEW_NODE_TYPE, - REPOSITORY_NODE_TYPE, REFERENCES_EDGE_TYPE, + REPOSITORY_NODE_TYPE, } from "./types"; import {PLUGIN_NAME} from "./pluginName"; import {COMMIT_NODE_TYPE} from "../git/types"; -export type Entity = - | Repository - | Issue - | PullRequest - | Comment - | Author - | PullRequestReview - | PullRequestReviewComment; - -function assertEntityType(e: Entity, t: NodeType) { - if (e.type() !== t) { +function assertAddressType(address: Address, t: NodeType) { + if (address.type !== t) { throw new Error( - `Expected entity at ${stringify(e.address())} to have type ${t}` + `Expected entity at ${stringify(address)} to have type ${t}` ); } } -export function asEntity( - g: Graph, - addr: Address -): Entity { - const type: NodeType = (addr.type: any); +function asGithubReference( + ref: NodeReference +): GithubReference { + const addr = ref.address(); if (addr.pluginName !== PLUGIN_NAME) { throw new Error( `Tried to make GitHub porcelain, but got the wrong plugin name: ${stringify( @@ -81,21 +71,22 @@ export function asEntity( )}` ); } + const type: NodeType = (addr.type: any); switch (type) { case "ISSUE": - return new Issue(g, addr); + return new IssueReference(ref); case "PULL_REQUEST": - return new PullRequest(g, addr); + return new PullRequestReference(ref); case "COMMENT": - return new Comment(g, addr); + return new CommentReference(ref); case "AUTHOR": - return new Author(g, addr); + return new AuthorReference(ref); case "PULL_REQUEST_REVIEW": - return new PullRequestReview(g, addr); + return new PullRequestReviewReference(ref); case "PULL_REQUEST_REVIEW_COMMENT": - return new PullRequestReviewComment(g, addr); + return new PullRequestReviewCommentReference(ref); case "REPOSITORY": - return new Repository(g, addr); + return new RepositoryReference(ref); default: // eslint-disable-next-line no-unused-expressions (type: empty); @@ -107,7 +98,7 @@ export function asEntity( } } -export class Porcelain { +export class GraphPorcelain { graph: Graph; constructor(graph: Graph) { @@ -115,216 +106,261 @@ export class Porcelain { } /* Return all the repositories in the graph */ - repositories(): Repository[] { + repositories(): RepositoryReference[] { return this.graph .nodes({type: REPOSITORY_NODE_TYPE}) - .map((n) => new Repository(this.graph, n.address)); + .map( + (n) => new RepositoryReference(new NodeReference(this.graph, n.address)) + ); } /* Return the repository with the given owner and name */ - repository(owner: string, name: string): Repository { - const repo = this.repositories().filter( - (r) => r.owner() === owner && r.name() === name - ); - if (repo.length > 1) { - throw new Error( - `Unexpectedly found multiple repositories named ${owner}/${name}` - ); + repository(owner: string, name: string): ?RepositoryReference { + for (const repo of this.repositories()) { + const repoNode = repo.get(); + if ( + repoNode != null && + repoNode.owner() === owner && + repoNode.name() === name + ) { + return repo; + } } - return repo[0]; - } - - authors(): Author[] { - return this.graph - .nodes({type: AUTHOR_NODE_TYPE}) - .map((n) => new Author(this.graph, n.address)); } } -class GithubEntity { - graph: Graph; - nodeAddress: Address; - - constructor(graph: Graph, nodeAddress: Address) { - this.graph = graph; - this.nodeAddress = nodeAddress; - } - - node(): Node { - return (this.graph.node(this.nodeAddress): Node); - } - - url(): string { - return this.node().payload.url; +export class GithubReference<+T: NodePayload> extends NodeReference { + constructor(ref: NodeReference) { + const addr = ref.address(); + if (addr.pluginName !== PLUGIN_NAME) { + throw new Error( + `Wrong plugin name ${addr.pluginName} for GitHub plugin!` + ); + } + super(ref.graph(), addr); } type(): NodeType { - return (this.nodeAddress.type: any); + return ((super.type(): string): any); } - address(): Address { - return this.nodeAddress; + get(): ?GithubPorcelain { + const nodePorcelain = super.get(); + if (nodePorcelain != null) { + return new GithubPorcelain(nodePorcelain); + } } } -export class Repository extends GithubEntity { - static from(e: Entity): Repository { - assertEntityType(e, REPOSITORY_NODE_TYPE); - return (e: any); +export class GithubPorcelain<+T: NodePayload> extends NodePorcelain { + constructor(nodePorcelain: NodePorcelain) { + if (nodePorcelain.ref().address().pluginName !== PLUGIN_NAME) { + throw new Error( + `Wrong plugin name ${ + nodePorcelain.ref().address().pluginName + } for GitHub plugin!` + ); + } + super(nodePorcelain.ref(), nodePorcelain.node()); } - issueByNumber(number: number): ?Issue { - for (const {neighbor} of this.graph.neighborhood(this.nodeAddress, { + url(): string { + return this.payload().url; + } +} + +export class RepositoryReference extends GithubReference< + RepositoryNodePayload +> { + constructor(ref: NodeReference) { + super(ref); + assertAddressType(ref.address(), REPOSITORY_NODE_TYPE); + } + + issueByNumber(number: number): ?IssueReference { + const neighbors = this.neighbors({ edgeType: CONTAINS_EDGE_TYPE, direction: "OUT", nodeType: ISSUE_NODE_TYPE, - })) { - const node = this.graph.node(neighbor); - if (node.payload.number === number) { - return new Issue(this.graph, neighbor); + }); + for (const {ref} of neighbors) { + const issueRef = new IssueReference(ref); + const node = issueRef.get(); + if (node != null && node.number() === number) { + return issueRef; } } } - pullRequestByNumber(number: number): ?PullRequest { - for (const {neighbor} of this.graph.neighborhood(this.nodeAddress, { + pullRequestByNumber(number: number): ?PullRequestReference { + const neighbors = this.neighbors({ edgeType: CONTAINS_EDGE_TYPE, direction: "OUT", nodeType: PULL_REQUEST_NODE_TYPE, - })) { - const node = this.graph.node(neighbor); - if (node.payload.number === number) { - return new PullRequest(this.graph, neighbor); + }); + for (const {ref} of neighbors) { + const pullRequest = new PullRequestReference(ref); + const node = pullRequest.get(); + if (node != null && node.number() === number) { + return pullRequest; } } } - owner(): string { - return this.node().payload.owner; + issues(): IssueReference[] { + return this.neighbors({ + edgeType: CONTAINS_EDGE_TYPE, + direction: "OUT", + nodeType: ISSUE_NODE_TYPE, + }).map(({ref}) => new IssueReference(ref)); } - name(): string { - return this.node().payload.name; + pullRequests(): PullRequestReference[] { + return this.neighbors({ + edgeType: CONTAINS_EDGE_TYPE, + direction: "OUT", + nodeType: PULL_REQUEST_NODE_TYPE, + }).map(({ref}) => new PullRequestReference(ref)); } - issues(): Issue[] { - return this.graph - .neighborhood(this.nodeAddress, { - direction: "OUT", - edgeType: CONTAINS_EDGE_TYPE, - nodeType: ISSUE_NODE_TYPE, - }) - .map(({neighbor}) => new Issue(this.graph, neighbor)); - } - - pullRequests(): PullRequest[] { - return this.graph - .neighborhood(this.nodeAddress, { - direction: "OUT", - edgeType: CONTAINS_EDGE_TYPE, - nodeType: PULL_REQUEST_NODE_TYPE, - }) - .map(({neighbor}) => new PullRequest(this.graph, neighbor)); + get(): ?RepositoryPorcelain { + const nodePorcelain = super.get(); + if (nodePorcelain != null) { + return new RepositoryPorcelain(nodePorcelain); + } } } -class Post< +export class RepositoryPorcelain extends GithubPorcelain< + RepositoryNodePayload +> { + constructor(nodePorcelain: NodePorcelain) { + assertAddressType(nodePorcelain.ref().address(), REPOSITORY_NODE_TYPE); + super(nodePorcelain); + } + + owner(): string { + return this.payload().owner; + } + + name(): string { + return this.payload().name; + } + + ref(): RepositoryReference { + return new RepositoryReference(super.ref()); + } +} + +class PostReference< T: | IssueNodePayload | PullRequestNodePayload | CommentNodePayload | PullRequestReviewNodePayload | PullRequestReviewCommentNodePayload -> extends GithubEntity { - authors(): Author[] { - return this.graph - .neighborhood(this.nodeAddress, { - edgeType: AUTHORS_EDGE_TYPE, - nodeType: AUTHOR_NODE_TYPE, - }) - .map(({neighbor}) => new Author(this.graph, neighbor)); +> extends GithubReference { + authors(): AuthorReference[] { + return this.neighbors({ + edgeType: AUTHORS_EDGE_TYPE, + nodeType: AUTHOR_NODE_TYPE, + }).map(({ref}) => new AuthorReference(ref)); } + references(): GithubReference[] { + return this.neighbors({ + edgeType: REFERENCES_EDGE_TYPE, + direction: "OUT", + }).map(({ref}) => asGithubReference(ref)); + } +} + +class PostPorcelain< + T: + | IssueNodePayload + | PullRequestNodePayload + | CommentNodePayload + | PullRequestReviewNodePayload + | PullRequestReviewCommentNodePayload +> extends GithubPorcelain { body(): string { - return this.node().payload.body; - } - - references(): Entity[] { - return this.graph - .neighborhood(this.nodeAddress, { - edgeType: REFERENCES_EDGE_TYPE, - direction: "OUT", - }) - .map(({neighbor}) => asEntity(this.graph, neighbor)); + return this.payload().body; } } -class Commentable extends Post< - T -> { - comments(): Comment[] { - return this.graph - .neighborhood(this.nodeAddress, { - edgeType: CONTAINS_EDGE_TYPE, - nodeType: COMMENT_NODE_TYPE, - }) - .map(({neighbor}) => new Comment(this.graph, neighbor)); +class CommentableReference< + T: IssueNodePayload | PullRequestNodePayload +> extends PostReference { + comments(): CommentReference[] { + return this.neighbors({ + edgeType: CONTAINS_EDGE_TYPE, + nodeType: COMMENT_NODE_TYPE, + direction: "OUT", + }).map(({ref}) => new CommentReference(ref)); } } -export class Author extends GithubEntity { - static from(e: Entity): Author { - assertEntityType(e, AUTHOR_NODE_TYPE); - return (e: any); +export class AuthorReference extends GithubReference { + constructor(ref: NodeReference) { + super(ref); + assertAddressType(ref.address(), AUTHOR_NODE_TYPE); } + get(): ?AuthorPorcelain { + const nodePorcelain = super.get(); + if (nodePorcelain != null) { + return new AuthorPorcelain(nodePorcelain); + } + } +} + +export class AuthorPorcelain extends GithubPorcelain { + constructor(nodePorcelain: NodePorcelain) { + assertAddressType(nodePorcelain.ref().address(), AUTHOR_NODE_TYPE); + super(nodePorcelain); + } login(): string { - return this.node().payload.login; + return this.payload().login; } subtype(): AuthorSubtype { - return this.node().payload.subtype; + return this.payload().subtype; + } + + ref(): AuthorReference { + return new AuthorReference(super.ref()); } } -export class PullRequest extends Commentable { - static from(e: Entity): PullRequest { - assertEntityType(e, PULL_REQUEST_NODE_TYPE); - return (e: any); +export class PullRequestReference extends CommentableReference< + PullRequestNodePayload +> { + constructor(ref: NodeReference) { + super(ref); + assertAddressType(ref.address(), PULL_REQUEST_NODE_TYPE); } - number(): number { - return this.node().payload.number; - } - - title(): string { - return this.node().payload.title; - } - - reviews(): PullRequestReview[] { - return this.graph - .neighborhood(this.nodeAddress, { - edgeType: CONTAINS_EDGE_TYPE, - nodeType: PULL_REQUEST_REVIEW_NODE_TYPE, - }) - .map(({neighbor}) => new PullRequestReview(this.graph, neighbor)); - } - - parent(): Repository { + parent(): RepositoryReference { return (_parent(this): any); } + reviews(): PullRequestReviewReference[] { + return this.neighbors({ + edgeType: CONTAINS_EDGE_TYPE, + nodeType: PULL_REQUEST_REVIEW_NODE_TYPE, + direction: "OUT", + }).map(({ref}) => new PullRequestReviewReference(ref)); + } + mergeCommitHash(): ?string { - const mergeEdge = this.graph - .neighborhood(this.nodeAddress, { - edgeType: MERGED_AS_EDGE_TYPE, - nodeType: COMMIT_NODE_TYPE, - direction: "OUT", - }) - .map(({edge}) => edge); + const mergeEdge = this.neighbors({ + edgeType: MERGED_AS_EDGE_TYPE, + nodeType: COMMIT_NODE_TYPE, + direction: "OUT", + }).map(({edge}) => edge); if (mergeEdge.length > 1) { throw new Error( - `Node at ${this.nodeAddress.id} has too many MERGED_AS edges` + `Node at ${stringify(this.address())} has too many MERGED_AS edges` ); } if (mergeEdge.length === 0) { @@ -333,81 +369,188 @@ export class PullRequest extends Commentable { const payload: MergedAsEdgePayload = (mergeEdge[0].payload: any); return payload.hash; } + + get(): ?PullRequestPorcelain { + const nodePorcelain = super.get(); + if (nodePorcelain != null) { + return new PullRequestPorcelain(nodePorcelain); + } + } } -export class Issue extends Commentable { - static from(e: Entity): Issue { - assertEntityType(e, ISSUE_NODE_TYPE); - return (e: any); +export class PullRequestPorcelain extends PostPorcelain< + PullRequestNodePayload +> { + constructor(nodePorcelain: NodePorcelain) { + assertAddressType(nodePorcelain.ref().address(), PULL_REQUEST_NODE_TYPE); + super(nodePorcelain); } - number(): number { - return this.node().payload.number; + return this.payload().number; } title(): string { - return this.node().payload.title; + return this.payload().title; } - parent(): Repository { - return (_parent(this): any); + ref(): PullRequestReference { + return new PullRequestReference(super.ref()); } } -export class Comment extends Post { - static from(e: Entity): Comment { - assertEntityType(e, COMMENT_NODE_TYPE); - return (e: any); +export class IssueReference extends CommentableReference { + constructor(ref: NodeReference) { + super(ref); + assertAddressType(ref.address(), ISSUE_NODE_TYPE); } - parent(): Issue | PullRequest { + parent(): RepositoryReference { return (_parent(this): any); } + + get(): ?IssuePorcelain { + const nodePorcelain = super.get(); + if (nodePorcelain != null) { + return new IssuePorcelain(nodePorcelain); + } + } } -export class PullRequestReview extends Post { - static from(e: Entity): PullRequestReview { - assertEntityType(e, PULL_REQUEST_REVIEW_NODE_TYPE); - return (e: any); +export class IssuePorcelain extends PostPorcelain { + constructor(nodePorcelain: NodePorcelain) { + assertAddressType(nodePorcelain.ref().address(), ISSUE_NODE_TYPE); + super(nodePorcelain); + } + number(): number { + return this.payload().number; + } + + title(): string { + return this.payload().title; + } + + ref(): IssueReference { + return new IssueReference(super.ref()); + } +} + +export class CommentReference extends PostReference { + constructor(ref: NodeReference) { + super(ref); + assertAddressType(ref.address(), COMMENT_NODE_TYPE); + } + + parent(): IssueReference | PullRequestReference { + return (_parent(this): any); + } + + get(): ?CommentPorcelain { + const nodePorcelain = super.get(); + if (nodePorcelain != null) { + return new CommentPorcelain(nodePorcelain); + } + } +} + +export class CommentPorcelain extends PostPorcelain { + constructor(nodePorcelain: NodePorcelain) { + assertAddressType(nodePorcelain.ref().address(), COMMENT_NODE_TYPE); + super(nodePorcelain); + } + ref(): CommentReference { + return new CommentReference(super.ref()); + } +} + +export class PullRequestReviewReference extends PostReference< + PullRequestReviewNodePayload +> { + constructor(ref: NodeReference) { + super(ref); + assertAddressType(ref.address(), PULL_REQUEST_REVIEW_NODE_TYPE); + } + + parent(): PullRequestReference { + return (_parent(this): any); + } + + comments(): PullRequestReviewCommentReference[] { + return this.neighbors({ + edgeType: CONTAINS_EDGE_TYPE, + nodeType: PULL_REQUEST_REVIEW_COMMENT_NODE_TYPE, + direction: "OUT", + }).map(({ref}) => new PullRequestReviewCommentReference(ref)); + } + + get(): ?PullRequestReviewPorcelain { + const nodePorcelain = super.get(); + if (nodePorcelain != null) { + return new PullRequestReviewPorcelain(nodePorcelain); + } + } +} + +export class PullRequestReviewPorcelain extends PostPorcelain< + PullRequestReviewNodePayload +> { + constructor(nodePorcelain: NodePorcelain) { + assertAddressType( + nodePorcelain.ref().address(), + PULL_REQUEST_REVIEW_NODE_TYPE + ); + super(nodePorcelain); } state(): PullRequestReviewState { - return this.node().payload.state; + return this.payload().state; } - parent(): PullRequest { - return (_parent(this): any); - } - - comments(): PullRequestReviewComment[] { - return this.graph - .neighborhood(this.nodeAddress, { - edgeType: CONTAINS_EDGE_TYPE, - nodeType: PULL_REQUEST_REVIEW_COMMENT_NODE_TYPE, - }) - .map(({neighbor}) => new PullRequestReviewComment(this.graph, neighbor)); + ref(): PullRequestReviewReference { + return new PullRequestReviewReference(super.ref()); } } -export class PullRequestReviewComment extends Post< +export class PullRequestReviewCommentReference extends PostReference< PullRequestReviewCommentNodePayload > { - static from(e: Entity): PullRequestReviewComment { - assertEntityType(e, PULL_REQUEST_REVIEW_COMMENT_NODE_TYPE); - return (e: any); + constructor(ref: NodeReference) { + super(ref); + assertAddressType(ref.address(), PULL_REQUEST_REVIEW_COMMENT_NODE_TYPE); } - parent(): PullRequestReview { + + parent(): PullRequestReviewReference { return (_parent(this): any); } + + get(): ?PullRequestReviewCommentPorcelain { + const nodePorcelain = super.get(); + if (nodePorcelain != null) { + return new PullRequestReviewCommentPorcelain(nodePorcelain); + } + } } -function _parent(x: Entity): Entity { - const parents = x.graph.neighborhood(x.address(), { - edgeType: "CONTAINS", - direction: "IN", - }); +export class PullRequestReviewCommentPorcelain extends PostPorcelain< + PullRequestReviewCommentNodePayload +> { + constructor(nodePorcelain: NodePorcelain) { + assertAddressType( + nodePorcelain.ref().address(), + PULL_REQUEST_REVIEW_COMMENT_NODE_TYPE + ); + super(nodePorcelain); + } + ref(): PullRequestReviewCommentReference { + return new PullRequestReviewCommentReference(super.ref()); + } +} + +function _parent( + x: GithubReference +): GithubReference { + const parents = x.neighbors({edgeType: CONTAINS_EDGE_TYPE, direction: "IN"}); if (parents.length !== 1) { throw new Error(`Bad parent relationships for ${stringify(x.address())}`); } - return asEntity(x.graph, parents[0].neighbor); + return asGithubReference(parents[0].ref); } diff --git a/src/plugins/github/porcelain.test.js b/src/plugins/github/porcelain.test.js index 46535f2..4ba03cc 100644 --- a/src/plugins/github/porcelain.test.js +++ b/src/plugins/github/porcelain.test.js @@ -1,20 +1,26 @@ // @flow -import type {Address} from "../../core/address"; import {parse} from "./parser"; import exampleRepoData from "./demoData/example-github.json"; -import type {Entity} from "./porcelain"; import { - asEntity, - Porcelain, - Repository, - Issue, - PullRequest, - PullRequestReview, - PullRequestReviewComment, - Comment, - Author, + AuthorReference, + AuthorPorcelain, + CommentReference, + CommentPorcelain, + GithubReference, + GraphPorcelain, + IssueReference, + IssuePorcelain, + PullRequestReference, + PullRequestPorcelain, + PullRequestReviewReference, + PullRequestReviewPorcelain, + PullRequestReviewCommentReference, + PullRequestReviewCommentPorcelain, + RepositoryReference, + RepositoryPorcelain, } from "./porcelain"; +import type {NodePayload} from "./types"; import { AUTHOR_NODE_TYPE, COMMENT_NODE_TYPE, @@ -26,14 +32,19 @@ import { import {nodeDescription} from "./render"; -import {PLUGIN_NAME} from "./pluginName"; - describe("GitHub porcelain", () => { const graph = parse(exampleRepoData); - const porcelain = new Porcelain(graph); - const repo = porcelain.repository("sourcecred", "example-github"); + const porcelain = new GraphPorcelain(graph); + const repoRef = porcelain.repository("sourcecred", "example-github"); + if (repoRef == null) { + throw new Error("Where did the repository go?"); + } + const repo = repoRef.get(); + if (repo == null) { + throw new Error("Where did the repository go?"); + } - function expectPropertiesToMatchSnapshot( + function expectPropertiesToMatchSnapshot string}>( entities: $ReadOnlyArray, extractor: (T) => mixed ) { @@ -47,36 +58,75 @@ describe("GitHub porcelain", () => { expect(urlToProperty).toMatchSnapshot(); } - function issueByNumber(n: number): Issue { - const result = repo.issueByNumber(n); + function issueByNumber(n: number): IssuePorcelain { + const ref = repo.ref().issueByNumber(n); + if (ref == null) { + throw new Error(`Expected issue #${n} to exist`); + } + const result = ref.get(); if (result == null) { - throw new Error(`Expected Issue #${n} to exist`); + throw new Error(`Expected issue #${n} to exist`); } return result; } - function prByNumber(n: number): PullRequest { - const result = repo.pullRequestByNumber(n); + function prByNumber(n: number): PullRequestPorcelain { + const ref = repo.ref().pullRequestByNumber(n); + if (ref == null) { + throw new Error(`Expected pull request #${n} to exist`); + } + const result = ref.get(); if (result == null) { - throw new Error(`Expected PR #${n} to exist`); + throw new Error(`Expected pull request #${n} to exist`); } return result; } - function issueOrPrByNumber(n: number): Issue | PullRequest { - const result = repo.issueByNumber(n) || repo.pullRequestByNumber(n); + function issueOrPrByNumber(n: number): IssuePorcelain | PullRequestPorcelain { + const ref = + repo.ref().issueByNumber(n) || repo.ref().pullRequestByNumber(n); + if (ref == null) { + throw new Error(`Expected Issue/PR #${n} to exist`); + } + const result = ref.get(); if (result == null) { throw new Error(`Expected Issue/PR #${n} to exist`); } return result; } - const issue = issueByNumber(2); - const comment = issue.comments()[0]; - const pullRequest = prByNumber(5); - const pullRequestReview = pullRequest.reviews()[0]; - const pullRequestReviewComment = pullRequestReview.comments()[0]; - const author = issue.authors()[0]; + function really(x: ?T): T { + if (x == null) { + throw new Error(String(x)); + } + return x; + } + const issue = really(issueByNumber(2)); + const comment = really( + issue + .ref() + .comments()[0] + .get() + ); + const pullRequest = really(prByNumber(5)); + const pullRequestReview = really( + pullRequest + .ref() + .reviews()[0] + .get() + ); + const pullRequestReviewComment = really( + pullRequestReview + .ref() + .comments()[0] + .get() + ); + const author = really( + issue + .ref() + .authors()[0] + .get() + ); const allWrappers = [ issue, pullRequest, @@ -87,62 +137,43 @@ describe("GitHub porcelain", () => { ]; it("all wrappers provide a type() method", () => { - expectPropertiesToMatchSnapshot(allWrappers, (e) => e.type()); + expectPropertiesToMatchSnapshot(allWrappers, (e) => e.ref().type()); }); it("all wrappers provide a url() method", () => { expectPropertiesToMatchSnapshot(allWrappers, (e) => e.url()); }); - it("all wrappers provide an address() method", () => { - allWrappers.forEach((w) => { - const addr = w.address(); - const url = w.url(); - const type = w.type(); - expect(addr.id).toBe(url); - expect(addr.type).toBe(type); - expect(addr.pluginName).toBe(PLUGIN_NAME); - }); + test("reference constructors throw errors when used incorrectly", () => { + expect(() => new RepositoryReference(issue.ref())).toThrowError( + "to have type" + ); + expect(() => new IssueReference(repo.ref())).toThrowError("to have type"); + expect(() => new CommentReference(repo.ref())).toThrowError("to have type"); + expect(() => new PullRequestReference(repo.ref())).toThrowError( + "to have type" + ); + expect(() => new PullRequestReviewReference(repo.ref())).toThrowError( + "to have type" + ); + expect( + () => new PullRequestReviewCommentReference(repo.ref()) + ).toThrowError("to have type"); + expect(() => new AuthorReference(repo.ref())).toThrowError("to have type"); }); - it("all wrappers provide a node() method", () => { - allWrappers.forEach((w) => { - const node = w.node(); - const addr = w.address(); - expect(node.address).toEqual(addr); - }); - }); - - describe("type verifiers", () => { - it("are provided by all wrappers", () => { - // Check each one individually to verify the flowtypes - const _unused_repo: Repository = Repository.from(repo); - const _unused_issue: Issue = Issue.from(issue); - const _unused_pullRequest: PullRequest = PullRequest.from(pullRequest); - const _unused_comment: Comment = Comment.from(comment); - const _unused_pullRequestReview: PullRequestReview = PullRequestReview.from( - pullRequestReview - ); - const _unused_pullRequestReviewComment: PullRequestReviewComment = PullRequestReviewComment.from( - pullRequestReviewComment - ); - const _unused_author: Author = Author.from(author); - // Check them programatically so that if we add another wrapper, we can't forget to update. - allWrappers.forEach((e) => { - expect(e.constructor.from(e)).toEqual(e); - }); - }); - it("and errors are thrown when used incorrectly", () => { - expect(() => Repository.from(issue)).toThrowError("to have type"); - expect(() => Issue.from(repo)).toThrowError("to have type"); - expect(() => Comment.from(repo)).toThrowError("to have type"); - expect(() => PullRequest.from(repo)).toThrowError("to have type"); - expect(() => PullRequestReview.from(repo)).toThrowError("to have type"); - expect(() => PullRequestReviewComment.from(repo)).toThrowError( - "to have type" - ); - expect(() => Author.from(repo)).toThrowError("to have type"); - }); + test("porcelain constructors throw errors when used incorrectly", () => { + expect(() => new RepositoryPorcelain(issue)).toThrowError("to have type"); + expect(() => new IssuePorcelain(repo)).toThrowError("to have type"); + expect(() => new CommentPorcelain(repo)).toThrowError("to have type"); + expect(() => new PullRequestPorcelain(repo)).toThrowError("to have type"); + expect(() => new PullRequestReviewPorcelain(repo)).toThrowError( + "to have type" + ); + expect(() => new PullRequestReviewCommentPorcelain(repo)).toThrowError( + "to have type" + ); + expect(() => new AuthorPorcelain(repo)).toThrowError("to have type"); }); describe("posts", () => { @@ -154,19 +185,32 @@ describe("GitHub porcelain", () => { comment, ]; it("have parents", () => { - expectPropertiesToMatchSnapshot(allPosts, (e) => e.parent().url()); + expectPropertiesToMatchSnapshot(allPosts, (e) => + really( + e + .ref() + .parent() + .get() + ).url() + ); }); it("have bodies", () => { expectPropertiesToMatchSnapshot(allPosts, (e) => e.body()); }); it("have authors", () => { expectPropertiesToMatchSnapshot(allPosts, (e) => - e.authors().map((a) => a.login()) + e + .ref() + .authors() + .map((a) => really(a.get()).login()) ); }); it("have references", () => { expectPropertiesToMatchSnapshot(allPosts, (e) => - e.references().map((r) => r.url()) + e + .ref() + .references() + .map((r: GithubReference) => really(r.get()).url()) ); }); }); @@ -183,7 +227,10 @@ describe("GitHub porcelain", () => { }); it("have comments", () => { expectPropertiesToMatchSnapshot(issuesAndPRs, (e) => - e.comments().map((c) => c.url()) + e + .ref() + .comments() + .map((c) => really(c.get()).url()) ); }); }); @@ -191,49 +238,36 @@ describe("GitHub porcelain", () => { describe("pull requests", () => { const prs = [prByNumber(3), prByNumber(5), prByNumber(9)]; it("have mergeCommitHashes", () => { - expectPropertiesToMatchSnapshot(prs, (e) => e.mergeCommitHash()); + expectPropertiesToMatchSnapshot(prs, (e) => e.ref().mergeCommitHash()); }); it("have reviews", () => { expectPropertiesToMatchSnapshot(prs, (e) => - e.reviews().map((r) => r.url()) + e + .ref() + .reviews() + .map((r) => really(r.get()).url()) ); }); }); describe("pull request reviews", () => { - const reviews = pullRequest.reviews(); + const reviews = pullRequest.ref().reviews(); it("have review comments", () => { - expectPropertiesToMatchSnapshot(reviews, (e) => - e.comments().map((e) => e.url()) + expectPropertiesToMatchSnapshot( + reviews.map((r) => really(r.get())), + (e) => + e + .ref() + .comments() + .map((e) => really(e.get()).url()) ); }); it("have states", () => { - expectPropertiesToMatchSnapshot(reviews, (e) => e.state()); - }); - }); - - describe("asEntity", () => { - it("works for each wrapper", () => { - allWrappers.forEach((w) => { - expect(asEntity(w.graph, w.address())).toEqual(w); - }); - }); - it("errors when given an address with the wrong plugin name", () => { - const addr: Address = { - pluginName: "the magnificent foo plugin", - id: "who are you to ask an id of the magnificent foo plugin?", - type: "ISSUE", - }; - expect(() => asEntity(graph, addr)).toThrow("wrong plugin name"); - }); - it("errors when given an address with a bad node type", () => { - const addr: Address = { - pluginName: PLUGIN_NAME, - id: "if you keep asking for my id you will make me angry", - type: "the foo plugin's magnificence extends to many plugins", - }; - expect(() => asEntity(graph, addr)).toThrow("invalid type"); + expectPropertiesToMatchSnapshot( + reviews.map((r) => really(r.get())), + (e) => e.state() + ); }); }); @@ -252,22 +286,24 @@ describe("GitHub porcelain", () => { describe("References", () => { it("via #-number", () => { const srcIssue = issueByNumber(2); - const references = srcIssue.references(); + const references = srcIssue.ref().references(); expect(references).toHaveLength(1); // Note: this verifies that we are not counting in-references, as // https://github.com/sourcecred/example-github/issues/6#issuecomment-385223316 // references #2. - const referenced = Issue.from(references[0]); + const referenced = new IssuePorcelain(really(references[0].get())); expect(referenced.number()).toBe(1); }); describe("by exact url", () => { function expectCommentToHaveSingleReference({commentNumber, type, url}) { - const comments = issueByNumber(2).comments(); + const comments = issueByNumber(2) + .ref() + .comments(); const references = comments[commentNumber].references(); expect(references).toHaveLength(1); - expect(references[0].url()).toBe(url); + expect(really(references[0].get()).url()).toBe(url); expect(references[0].type()).toBe(type); } @@ -324,6 +360,7 @@ describe("GitHub porcelain", () => { it("to multiple entities", () => { const references = issueByNumber(2) + .ref() .comments()[6] .references(); expect(references).toHaveLength(5); @@ -331,6 +368,7 @@ describe("GitHub porcelain", () => { it("to no entities", () => { const references = issueByNumber(2) + .ref() .comments()[7] .references(); expect(references).toHaveLength(0); @@ -339,10 +377,11 @@ describe("GitHub porcelain", () => { it("References by @-author", () => { const pr = prByNumber(5); - const references = pr.references(); + const references = pr.ref().references(); expect(references).toHaveLength(1); - const referenced = Author.from(references[0]); - expect(referenced.login()).toBe("wchargin"); + const referenced = new AuthorReference(references[0]); + const login = really(referenced.get()).login(); + expect(login).toBe("wchargin"); }); }); @@ -352,7 +391,7 @@ describe("GitHub porcelain", () => { // file, and move this test to render.test.js (assuming we don't move the // description method into the porcelain anyway...) expectPropertiesToMatchSnapshot(allWrappers, (e) => - nodeDescription(e.graph, e.address()) + nodeDescription(e.ref()) ); }); }); diff --git a/src/plugins/github/render.js b/src/plugins/github/render.js index acd73c2..6aef681 100644 --- a/src/plugins/github/render.js +++ b/src/plugins/github/render.js @@ -4,64 +4,82 @@ * Methods for rendering and displaying GitHub nodes. */ import stringify from "json-stable-stringify"; -import {Graph} from "../../core/graph"; -import type {Address} from "../../core/address"; +import type {NodeReference} from "../../core/porcelain"; +import type {NodePayload} from "./types"; import { - asEntity, - Issue, - PullRequest, - Comment, - PullRequestReview, - PullRequestReviewComment, - Author, - Repository, + GithubReference, + AuthorReference, + AuthorPorcelain, + CommentReference, + IssueReference, + IssuePorcelain, + PullRequestReference, + PullRequestPorcelain, + PullRequestReviewReference, + PullRequestReviewCommentReference, + RepositoryPorcelain, } from "./porcelain"; /* Give a short description for the GitHub node at given address. * Useful for e.g. displaying a title. */ -export function nodeDescription(graph: Graph, addr: Address) { - const entity = asEntity(graph, addr); - const type = entity.type(); +export function nodeDescription(ref: NodeReference) { + const porcelain = ref.get(); + if (porcelain == null) { + return `[unknown ${ref.type()}]`; + } + const type = new GithubReference(ref).type(); switch (type) { case "REPOSITORY": { - const repo = Repository.from(entity); + const repo = new RepositoryPorcelain(porcelain); return `${repo.owner()}/${repo.name()}`; } case "ISSUE": { - const issue = Issue.from(entity); + const issue = new IssuePorcelain(porcelain); return `#${issue.number()}: ${issue.title()}`; } case "PULL_REQUEST": { - const pr = PullRequest.from(entity); + const pr = new PullRequestPorcelain(porcelain); return `#${pr.number()}: ${pr.title()}`; } case "COMMENT": { - const comment = Comment.from(entity); - const author = comment.authors()[0]; - return `comment by @${author.login()} on #${comment.parent().number()}`; + const comment = new CommentReference(ref); + const issue = comment.parent(); + return `comment by @${authors(comment)} on #${num(issue)}`; } case "PULL_REQUEST_REVIEW": { - const review = PullRequestReview.from(entity); - const author = review.authors()[0]; - return `review by @${author.login()} on #${review.parent().number()}`; + const review = new PullRequestReviewReference(ref); + const pr = review.parent(); + return `review by @${authors(review)} on #${num(pr)}`; } case "PULL_REQUEST_REVIEW_COMMENT": { - const comment = PullRequestReviewComment.from(entity); - const author = comment.authors()[0]; + const comment = new PullRequestReviewCommentReference(ref); const pr = comment.parent().parent(); - return `review comment by @${author.login()} on #${pr.number()}`; + return `review comment by @${authors(comment)} on #${num(pr)}`; } case "AUTHOR": { - const author = Author.from(entity); - return `@${author.login()}`; + return `@${new AuthorPorcelain(porcelain).login()}`; } default: { // eslint-disable-next-line no-unused-expressions (type: empty); throw new Error( - `Tried to write description for invalid type ${stringify(addr)}` + `Tried to write description for invalid type ${stringify( + ref.address() + )}` ); } } } + +function num(x: IssueReference | PullRequestReference) { + const np = x.get(); + return np == null ? "[unknown]" : np.number(); +} + +function authors(authorable: {+authors: () => AuthorReference[]}) { + // TODO: modify to accomodate multi-authorship + const authorRefs = authorable.authors(); + const firstAuthor = authorRefs[0].get(); + return firstAuthor != null ? firstAuthor.login() : "[unknown]"; +}