lib/github: better type safety on json accesses
[nit.git] / lib / github / api.nit
index 138b079..84badf7 100644 (file)
@@ -12,7 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Nit object oriented interface to Github api.
+# Nit object oriented interface to [Github api](https://developer.github.com/v3/).
 #
 # This modules reifies Github API elements as Nit classes.
 #
 #
 # This modules reifies Github API elements as Nit classes.
 #
@@ -21,11 +21,9 @@ module api
 
 import github_curl
 
 
 import github_curl
 
-# Interface to Github REST API.
+# Client to Github API
 #
 #
-# Used by all `GithubEntity` to perform requests.
-#
-# Usage:
+# To access the API you need an instance of a `GithubAPI` client.
 #
 # ~~~
 # # Get Github authentification token.
 #
 # ~~~
 # # Get Github authentification token.
@@ -36,22 +34,29 @@ import github_curl
 # var api = new GithubAPI(token)
 # ~~~
 #
 # var api = new GithubAPI(token)
 # ~~~
 #
-# The API client allows to get Github API entities:
+# The API client allows you to get Github API entities.
 #
 # ~~~
 #
 # ~~~
-# var repo = api.load_repo("privat/nit")
-# assert repo isa Repo
+# var repo = api.load_repo("nitlang/nit")
+# assert repo != null
 # assert repo.name == "nit"
 #
 # var user = api.load_user("Morriar")
 # assert repo.name == "nit"
 #
 # var user = api.load_user("Morriar")
-# assert user isa User
+# assert user != null
 # assert user.login == "Morriar"
 # ~~~
 class GithubAPI
 
 # assert user.login == "Morriar"
 # ~~~
 class GithubAPI
 
-       # Github API OAuth token.
+       # Github API OAuth token
+       #
+       # To access your private ressources, you must
+       # [authenticate](https://developer.github.com/guides/basics-of-authentication/).
+       #
+       # For client applications, Github recommands to use the
+       # [OAuth tokens](https://developer.github.com/v3/oauth/) authentification method.
+       #
+       #
        #
        #
-       # This token is used to authenticate the application on Github API.
        # Be aware that there is [rate limits](https://developer.github.com/v3/rate_limit/)
        # associated to the key.
        var auth: String
        # Be aware that there is [rate limits](https://developer.github.com/v3/rate_limit/)
        # associated to the key.
        var auth: String
@@ -89,7 +94,7 @@ class GithubAPI
        # See other `load_*` methods to use more expressive types.
        #
        #     var api = new GithubAPI(get_github_oauth)
        # See other `load_*` methods to use more expressive types.
        #
        #     var api = new GithubAPI(get_github_oauth)
-       #     var obj = api.get("repos/privat/nit")
+       #     var obj = api.get("repos/nitlang/nit")
        #     assert obj isa JsonObject
        #     assert obj["name"] == "nit"
        #
        #     assert obj isa JsonObject
        #     assert obj["name"] == "nit"
        #
@@ -133,41 +138,37 @@ class GithubAPI
 
        # Load the json object from Github.
        # See `GithubEntity::load_from_github`.
 
        # Load the json object from Github.
        # See `GithubEntity::load_from_github`.
-       private fun load_from_github(key: String): JsonObject do
+       protected fun load_from_github(key: String): JsonObject do
                message(1, "Get {key} (github)")
                var res = get(key)
                if was_error then return new JsonObject
                return res.as(JsonObject)
        end
 
                message(1, "Get {key} (github)")
                var res = get(key)
                if was_error then return new JsonObject
                return res.as(JsonObject)
        end
 
-       # Get the Github user with `login`.
+       # Get the Github user with `login`
        #
        #
-       # Returns `null` if the user cannot be found.
+       # Loads the `User` from the API or returns `null` if the user cannot be found.
        #
        #     var api = new GithubAPI(get_github_oauth)
        #     var user = api.load_user("Morriar")
        #     assert user.login == "Morriar"
        fun load_user(login: String): nullable User do
                var user = new User(self, login)
        #
        #     var api = new GithubAPI(get_github_oauth)
        #     var user = api.load_user("Morriar")
        #     assert user.login == "Morriar"
        fun load_user(login: String): nullable User do
                var user = new User(self, login)
-               user.load_from_github
-               if was_error then return null
-               return user
+               return user.load_from_github
        end
 
        # Get the Github repo with `full_name`.
        #
        end
 
        # Get the Github repo with `full_name`.
        #
-       # Returns `null` if the repo cannot be found.
+       # Loads the `Repo` from the API or returns `null` if the repo cannot be found.
        #
        #     var api = new GithubAPI(get_github_oauth)
        #
        #     var api = new GithubAPI(get_github_oauth)
-       #     var repo = api.load_repo("privat/nit")
+       #     var repo = api.load_repo("nitlang/nit")
        #     assert repo.name == "nit"
        #     assert repo.name == "nit"
-       #     assert repo.owner.login == "privat"
+       #     assert repo.owner.login == "nitlang"
        #     assert repo.default_branch.name == "master"
        fun load_repo(full_name: String): nullable Repo do
                var repo = new Repo(self, full_name)
        #     assert repo.default_branch.name == "master"
        fun load_repo(full_name: String): nullable Repo do
                var repo = new Repo(self, full_name)
-               repo.load_from_github
-               if was_error then return null
-               return repo
+               return repo.load_from_github
        end
 
        # Get the Github branch with `name`.
        end
 
        # Get the Github branch with `name`.
@@ -175,16 +176,14 @@ class GithubAPI
        # Returns `null` if the branch cannot be found.
        #
        #     var api = new GithubAPI(get_github_oauth)
        # Returns `null` if the branch cannot be found.
        #
        #     var api = new GithubAPI(get_github_oauth)
-       #     var repo = api.load_repo("privat/nit")
-       #     assert repo isa Repo
+       #     var repo = api.load_repo("nitlang/nit")
+       #     assert repo != null
        #     var branch = api.load_branch(repo, "master")
        #     assert branch.name == "master"
        #     assert branch.commit isa Commit
        fun load_branch(repo: Repo, name: String): nullable Branch do
                var branch = new Branch(self, repo, name)
        #     var branch = api.load_branch(repo, "master")
        #     assert branch.name == "master"
        #     assert branch.commit isa Commit
        fun load_branch(repo: Repo, name: String): nullable Branch do
                var branch = new Branch(self, repo, name)
-               branch.load_from_github
-               if was_error then return null
-               return branch
+               return branch.load_from_github
        end
 
        # Get the Github commit with `sha`.
        end
 
        # Get the Github commit with `sha`.
@@ -192,15 +191,42 @@ class GithubAPI
        # Returns `null` if the commit cannot be found.
        #
        #     var api = new GithubAPI(get_github_oauth)
        # Returns `null` if the commit cannot be found.
        #
        #     var api = new GithubAPI(get_github_oauth)
-       #     var repo = api.load_repo("privat/nit")
-       #     assert repo isa Repo
+       #     var repo = api.load_repo("nitlang/nit")
+       #     assert repo != null
        #     var commit = api.load_commit(repo, "64ce1f")
        #     assert commit isa Commit
        fun load_commit(repo: Repo, sha: String): nullable Commit do
                var commit = new Commit(self, repo, sha)
        #     var commit = api.load_commit(repo, "64ce1f")
        #     assert commit isa Commit
        fun load_commit(repo: Repo, sha: String): nullable Commit do
                var commit = new Commit(self, repo, sha)
-               commit.load_from_github
-               if was_error then return null
-               return commit
+               return commit.load_from_github
+       end
+
+       # Get the Github issue #`number`.
+       #
+       # Returns `null` if the issue cannot be found.
+       #
+       #     var api = new GithubAPI(get_github_oauth)
+       #     var repo = api.load_repo("nitlang/nit")
+       #     assert repo != null
+       #     var issue = api.load_issue(repo, 1)
+       #     assert issue.title == "Doc"
+       fun load_issue(repo: Repo, number: Int): nullable Issue do
+               var issue = new Issue(self, repo, number)
+               return issue.load_from_github
+       end
+
+       # Get the Github pull request #`number`.
+       #
+       # Returns `null` if the pull request cannot be found.
+       #
+       #     var api = new GithubAPI(get_github_oauth)
+       #     var repo = api.load_repo("nitlang/nit")
+       #     assert repo != null
+       #     var pull = api.load_pull(repo, 1)
+       #     assert pull.title == "Doc"
+       #     assert pull.user.login == "Morriar"
+       fun load_pull(repo: Repo, number: Int): nullable PullRequest do
+               var pull = new PullRequest(self, repo, number)
+               return pull.load_from_github
        end
 
        # Get the Github label with `name`.
        end
 
        # Get the Github label with `name`.
@@ -208,15 +234,13 @@ class GithubAPI
        # Returns `null` if the label cannot be found.
        #
        #     var api = new GithubAPI(get_github_oauth)
        # Returns `null` if the label cannot be found.
        #
        #     var api = new GithubAPI(get_github_oauth)
-       #     var repo = api.load_repo("privat/nit")
-       #     assert repo isa Repo
+       #     var repo = api.load_repo("nitlang/nit")
+       #     assert repo != null
        #     var labl = api.load_label(repo, "ok_will_merge")
        #     assert labl != null
        fun load_label(repo: Repo, name: String): nullable Label do
                var labl = new Label(self, repo, name)
        #     var labl = api.load_label(repo, "ok_will_merge")
        #     assert labl != null
        fun load_label(repo: Repo, name: String): nullable Label do
                var labl = new Label(self, repo, name)
-               labl.load_from_github
-               if was_error then return null
-               return labl
+               return labl.load_from_github
        end
 
        # Get the Github milestone with `id`.
        end
 
        # Get the Github milestone with `id`.
@@ -224,15 +248,78 @@ class GithubAPI
        # Returns `null` if the milestone cannot be found.
        #
        #     var api = new GithubAPI(get_github_oauth)
        # Returns `null` if the milestone cannot be found.
        #
        #     var api = new GithubAPI(get_github_oauth)
-       #     var repo = api.load_repo("privat/nit")
-       #     assert repo isa Repo
+       #     var repo = api.load_repo("nitlang/nit")
+       #     assert repo != null
        #     var stone = api.load_milestone(repo, 4)
        #     assert stone.title == "v1.0prealpha"
        fun load_milestone(repo: Repo, id: Int): nullable Milestone do
                var milestone = new Milestone(self, repo, id)
        #     var stone = api.load_milestone(repo, 4)
        #     assert stone.title == "v1.0prealpha"
        fun load_milestone(repo: Repo, id: Int): nullable Milestone do
                var milestone = new Milestone(self, repo, id)
-               milestone.load_from_github
-               if was_error then return null
-               return milestone
+               return milestone.load_from_github
+       end
+
+       # Get the Github issue event with `id`.
+       #
+       # Returns `null` if the event cannot be found.
+       #
+       #     var api = new GithubAPI(get_github_oauth)
+       #     var repo = api.load_repo("nitlang/nit")
+       #     assert repo isa Repo
+       #     var event = api.load_issue_event(repo, 199674194)
+       #     assert event.actor.login == "privat"
+       #     assert event.event == "labeled"
+       #     assert event.labl.name == "need_review"
+       #     assert event.issue.number == 945
+       fun load_issue_event(repo: Repo, id: Int): nullable IssueEvent do
+               var event = new IssueEvent(self, repo, id)
+               return event.load_from_github
+       end
+
+       # Get the Github commit comment with `id`.
+       #
+       # Returns `null` if the comment cannot be found.
+       #
+       #     var api = new GithubAPI(get_github_oauth)
+       #     var repo = api.load_repo("nitlang/nit")
+       #     assert repo != null
+       #     var comment = api.load_commit_comment(repo, 8982707)
+       #     assert comment.user.login == "Morriar"
+       #     assert comment.body == "For testing purposes..."
+       #     assert comment.commit.sha == "7eacb86d1e24b7e72bc9ac869bf7182c0300ceca"
+       fun load_commit_comment(repo: Repo, id: Int): nullable CommitComment do
+               var comment = new CommitComment(self, repo, id)
+               return comment.load_from_github
+       end
+
+       # Get the Github issue comment with `id`.
+       #
+       # Returns `null` if the comment cannot be found.
+       #
+       #     var api = new GithubAPI(get_github_oauth)
+       #     var repo = api.load_repo("nitlang/nit")
+       #     assert repo != null
+       #     var comment = api.load_issue_comment(repo, 6020149)
+       #     assert comment.user.login == "privat"
+       #     assert comment.created_at.to_s == "2012-05-30T20:16:54Z"
+       #     assert comment.issue.number == 10
+       fun load_issue_comment(repo: Repo, id: Int): nullable IssueComment do
+               var comment = new IssueComment(self, repo, id)
+               return comment.load_from_github
+       end
+
+       # Get the Github diff comment with `id`.
+       #
+       # Returns `null` if the comment cannot be found.
+       #
+       #     var api = new GithubAPI(get_github_oauth)
+       #     var repo = api.load_repo("nitlang/nit")
+       #     assert repo != null
+       #     var comment = api.load_review_comment(repo, 21010363)
+       #     assert comment.path == "src/modelize/modelize_property.nit"
+       #     assert comment.original_position == 26
+       #     assert comment.pull.number == 945
+       fun load_review_comment(repo: Repo, id: Int): nullable ReviewComment do
+               var comment = new ReviewComment(self, repo, id)
+               return comment.load_from_github
        end
 end
 
        end
 end
 
@@ -255,18 +342,22 @@ abstract class GithubEntity
        var json: JsonObject is noinit, protected writable
 
        # Load `json` from Github API.
        var json: JsonObject is noinit, protected writable
 
        # Load `json` from Github API.
-       private fun load_from_github do
+       private fun load_from_github: nullable SELF do
                json = api.load_from_github(key)
                json = api.load_from_github(key)
+               if api.was_error then return null
+               return self
        end
 
        redef fun to_s do return json.to_json
        end
 
        redef fun to_s do return json.to_json
+
+       # Github page url.
+       fun html_url: String do return json["html_url"].as(String)
 end
 
 end
 
-# A Github user.
+# A Github user
 #
 #
+# Provides access to [Github user data](https://developer.github.com/v3/users/).
 # Should be accessed from `GithubAPI::load_user`.
 # Should be accessed from `GithubAPI::load_user`.
-#
-# See <https://developer.github.com/v3/users/>.
 class User
        super GithubEntity
 
 class User
        super GithubEntity
 
@@ -277,22 +368,18 @@ class User
 
        # Init `self` from a `json` object.
        init from_json(api: GithubAPI, json: JsonObject) do
 
        # Init `self` from a `json` object.
        init from_json(api: GithubAPI, json: JsonObject) do
-               init(api, json["login"].to_s)
+               init(api, json["login"].as(String))
                self.json = json
        end
 
                self.json = json
        end
 
-       # Github User page url.
-       fun html_url: String do return json["html_url"].to_s
-
        # Avatar image url for this user.
        # Avatar image url for this user.
-       fun avatar_url: String do return json["avatar_url"].to_s
+       fun avatar_url: String do return json["avatar_url"].as(String)
 end
 
 # A Github repository.
 #
 end
 
 # A Github repository.
 #
+# Provides access to [Github repo data](https://developer.github.com/v3/repos/).
 # Should be accessed from `GithubAPI::load_repo`.
 # Should be accessed from `GithubAPI::load_repo`.
-#
-# See <https://developer.github.com/v3/repos/>.
 class Repo
        super GithubEntity
 
 class Repo
        super GithubEntity
 
@@ -303,15 +390,12 @@ class Repo
 
        # Init `self` from a `json` object.
        init from_json(api: GithubAPI, json: JsonObject) do
 
        # Init `self` from a `json` object.
        init from_json(api: GithubAPI, json: JsonObject) do
-               init(api, json["full_name"].to_s)
+               init(api, json["full_name"].as(String))
                self.json = json
        end
 
        # Repo short name on Github.
                self.json = json
        end
 
        # Repo short name on Github.
-       fun name: String do return json["name"].to_s
-
-       # Github User page url.
-       fun html_url: String do return json["html_url"].to_s
+       fun name: String do return json["name"].as(String)
 
        # Get the repo owner.
        fun owner: User do
 
        # Get the repo owner.
        fun owner: User do
@@ -326,12 +410,58 @@ class Repo
                if not array isa JsonArray then return res
                for obj in array do
                        if not obj isa JsonObject then continue
                if not array isa JsonArray then return res
                for obj in array do
                        if not obj isa JsonObject then continue
-                       var name = obj["name"].to_s
+                       var name = obj["name"].as(String)
                        res[name] = new Branch.from_json(api, self, obj)
                end
                return res
        end
 
                        res[name] = new Branch.from_json(api, self, obj)
                end
                return res
        end
 
+       # List of issues associated with their ids.
+       fun issues: Map[Int, Issue] do
+               api.message(1, "Get issues for {full_name}")
+               var res = new HashMap[Int, Issue]
+               var issue = last_issue
+               if issue == null then return res
+               res[issue.number] = issue
+               while issue.number > 1 do
+                       issue = api.load_issue(self, issue.number - 1)
+                       assert issue isa Issue
+                       res[issue.number] = issue
+               end
+               return res
+       end
+
+       # Search issues in this repo form an advanced query.
+       #
+       # Example:
+       #
+       # ~~~nitish
+       # var issues = repo.search_issues("is:open label:need_review")
+       # ~~~
+       #
+       # See <https://developer.github.com/v3/search/#search-issues>.
+       fun search_issues(query: String): nullable Array[Issue] do
+               query = "search/issues?q={query} repo:{full_name}"
+               var response = api.get(query)
+               if api.was_error then return null
+               var arr = response.as(JsonObject)["items"].as(JsonArray)
+               var res = new Array[Issue]
+               for obj in arr do
+                       res.add new Issue.from_json(api, self, obj.as(JsonObject))
+               end
+               return res
+       end
+
+       # Get the last published issue.
+       fun last_issue: nullable Issue do
+               var array = api.get("repos/{full_name}/issues")
+               if not array isa JsonArray then return null
+               if array.is_empty then return null
+               var obj = array.first
+               if not obj isa JsonObject then return null
+               return new Issue.from_json(api, self, obj)
+       end
+
        # List of labels associated with their names.
        fun labels: Map[String, Label] do
                api.message(1, "Get labels for {full_name}")
        # List of labels associated with their names.
        fun labels: Map[String, Label] do
                api.message(1, "Get labels for {full_name}")
@@ -340,7 +470,7 @@ class Repo
                if not array isa JsonArray then return res
                for obj in array do
                        if not obj isa JsonObject then continue
                if not array isa JsonArray then return res
                for obj in array do
                        if not obj isa JsonObject then continue
-                       var name = obj["name"].to_s
+                       var name = obj["name"].as(String)
                        res[name] = new Label.from_json(api, self, obj)
                end
                return res
                        res[name] = new Label.from_json(api, self, obj)
                end
                return res
@@ -361,9 +491,44 @@ class Repo
                return res
        end
 
                return res
        end
 
+       # List of pull-requests associated with their ids.
+       #
+       # Implementation notes: because PR numbers are not consecutive,
+       # PR are loaded from pages.
+       # See: https://developer.github.com/v3/pulls/#list-pull-requests
+       fun pulls: Map[Int, PullRequest] do
+               api.message(1, "Get pulls for {full_name}")
+               var res = new HashMap[Int, PullRequest]
+               var page = 1
+               var array = api.get("{key}/pulls?page={page}").as(JsonArray)
+               while not array.is_empty do
+                       for obj in array do
+                               if not obj isa JsonObject then continue
+                               var number = obj["number"].as(Int)
+                               res[number] = new PullRequest.from_json(api, self, obj)
+                       end
+                       page += 1
+                       array = api.get("{key}/pulls?page={page}").as(JsonArray)
+               end
+               return res
+       end
+
+       # List of contributor related statistics.
+       fun contrib_stats: Array[ContributorStats] do
+               api.message(1, "Get contributor stats for {full_name}")
+               var res = new Array[ContributorStats]
+               var array = api.get("{key}/stats/contributors")
+               if array isa JsonArray then
+                       for obj in array do
+                               res.add new ContributorStats.from_json(api, obj.as(JsonObject))
+                       end
+               end
+               return res
+       end
+
        # Repo default branch.
        fun default_branch: Branch do
        # Repo default branch.
        fun default_branch: Branch do
-               var name = json["default_branch"].to_s
+               var name = json["default_branch"].as(String)
                var branch = api.load_branch(self, name)
                assert branch isa Branch
                return branch
                var branch = api.load_branch(self, name)
                assert branch isa Branch
                return branch
@@ -399,7 +564,7 @@ class Branch
        var name: String
 
        redef init from_json(api, repo, json) do
        var name: String
 
        redef init from_json(api, repo, json) do
-               self.name = json["name"].to_s
+               self.name = json["name"].as(String)
                super
        end
 
                super
        end
 
@@ -434,7 +599,7 @@ end
 #
 # Should be accessed from `GithubAPI::load_commit`.
 #
 #
 # Should be accessed from `GithubAPI::load_commit`.
 #
-# See <https://developer.github.com/v3/commits/>.
+# See <https://developer.github.com/v3/repos/commits/>.
 class Commit
        super RepoEntity
 
 class Commit
        super RepoEntity
 
@@ -444,54 +609,291 @@ class Commit
        var sha: String
 
        redef init from_json(api, repo, json) do
        var sha: String
 
        redef init from_json(api, repo, json) do
-               self.sha = json["sha"].to_s
+               self.sha = json["sha"].as(String)
                super
        end
 
        # Parent commits of `self`.
        fun parents: Array[Commit] do
                var res = new Array[Commit]
                super
        end
 
        # Parent commits of `self`.
        fun parents: Array[Commit] do
                var res = new Array[Commit]
-               var parents = json["parents"]
+               var parents = json.get_or_null("parents")
                if not parents isa JsonArray then return res
                for obj in parents do
                        if not obj isa JsonObject then continue
                if not parents isa JsonArray then return res
                for obj in parents do
                        if not obj isa JsonObject then continue
-                       res.add(api.load_commit(repo, obj["sha"].to_s).as(not null))
+                       res.add(api.load_commit(repo, obj["sha"].as(String)).as(not null))
                end
                return res
        end
 
        # Author of the commit.
        fun author: nullable User do
                end
                return res
        end
 
        # Author of the commit.
        fun author: nullable User do
-               if not json.has_key("author") then return null
-               var user = json["author"]
-               if not user isa JsonObject then return null
-               return new User.from_json(api, user)
+               var user = json.get_or_null("author")
+               if user isa JsonObject then return new User.from_json(api, user)
+               return null
        end
 
        # Committer of the commit.
        fun committer: nullable User do
        end
 
        # Committer of the commit.
        fun committer: nullable User do
-               if not json.has_key("committer") then return null
-               var user = json["author"]
-               if not user isa JsonObject then return null
-               return new User.from_json(api, user)
+               var user = json.get_or_null("author")
+               if user isa JsonObject then return new User.from_json(api, user)
+               return null
        end
 
        # Authoring date as ISODate.
        fun author_date: ISODate do
                var commit = json["commit"].as(JsonObject)
                var author = commit["author"].as(JsonObject)
        end
 
        # Authoring date as ISODate.
        fun author_date: ISODate do
                var commit = json["commit"].as(JsonObject)
                var author = commit["author"].as(JsonObject)
-               return new ISODate.from_string(author["date"].to_s)
+               return new ISODate.from_string(author["date"].as(String))
        end
 
        # Commit date as ISODate.
        fun commit_date: ISODate do
                var commit = json["commit"].as(JsonObject)
                var author = commit["committer"].as(JsonObject)
        end
 
        # Commit date as ISODate.
        fun commit_date: ISODate do
                var commit = json["commit"].as(JsonObject)
                var author = commit["committer"].as(JsonObject)
-               return new ISODate.from_string(author["date"].to_s)
+               return new ISODate.from_string(author["date"].as(String))
+       end
+
+       # List files staged in this commit.
+       fun files: Array[GithubFile] do
+               var res = new Array[GithubFile]
+               var files = json.get_or_null("files")
+               if not files isa JsonArray then return res
+               for obj in files do
+                       res.add(new GithubFile(obj.as(JsonObject)))
+               end
+               return res
        end
 
        # Commit message.
        end
 
        # Commit message.
-       fun message: String do return json["commit"].as(JsonObject)["message"].to_s
+       fun message: String do return json["commit"].as(JsonObject)["message"].as(String)
+end
+
+# A Github issue.
+#
+# Should be accessed from `GithubAPI::load_issue`.
+#
+# See <https://developer.github.com/v3/issues/>.
+class Issue
+       super RepoEntity
+
+       redef var key is lazy do return "{repo.key}/issues/{number}"
+
+       # Issue Github ID.
+       var number: Int
+
+       redef init from_json(api, repo, json) do
+               self.number = json["number"].as(Int)
+               super
+       end
+
+       # Issue title.
+       fun title: String do return json["title"].as(String)
+
+       # User that created this issue.
+       fun user: User do
+               return new User.from_json(api, json["user"].as(JsonObject))
+       end
+
+       # List of labels on this issue associated to their names.
+       fun labels: Map[String, Label] do
+               var res = new HashMap[String, Label]
+               var lbls = json.get_or_null("labels")
+               if not lbls isa JsonArray then return res
+               for obj in lbls do
+                       if not obj isa JsonObject then continue
+                       var name = obj["name"].as(String)
+                       res[name] = new Label.from_json(api, repo, obj)
+               end
+               return res
+       end
+
+       # State of the issue on Github.
+       fun state: String do return json["state"].as(String)
+
+       # Is the issue locked?
+       fun locked: Bool do return json["locked"].as(Bool)
+
+       # Assigned `User` (if any).
+       fun assignee: nullable User do
+               var assignee = json.get_or_null("assignee")
+               if assignee isa JsonObject then return new User.from_json(api, assignee)
+               return null
+       end
+
+       # `Milestone` (if any).
+       fun milestone: nullable Milestone do
+               var milestone = json.get_or_null("milestone")
+               if milestone isa JsonObject then return new Milestone.from_json(api, repo, milestone)
+               return null
+       end
+
+       # List of comments made on this issue.
+       fun comments: Array[IssueComment] do
+               var res = new Array[IssueComment]
+               var count = comments_count
+               var page = 1
+               var array = api.get("{key}/comments?page={page}")
+               if not array isa JsonArray then
+                       return res
+               end
+               while not array.is_empty and res.length < count do
+                       for obj in array do
+                               if not obj isa JsonObject then continue
+                               var id = obj["id"].as(Int)
+                               res.add(api.load_issue_comment(repo, id).as(not null))
+                       end
+                       page += 1
+                       array = api.get("{key}/comments?page={page}").as(JsonArray)
+               end
+               return res
+       end
+
+       # Number of comments on this issue.
+       fun comments_count: Int do return json["comments"].as(Int)
+
+       # Creation time in ISODate format.
+       fun created_at: ISODate do
+               return new ISODate.from_string(json["created_at"].as(String))
+       end
+
+       # Last update time in ISODate format (if any).
+       fun updated_at: nullable ISODate do
+               var res = json.get_or_null("updated_at")
+               if res isa String then return new ISODate.from_string(res)
+               return null
+       end
+
+       # Close time in ISODate format (if any).
+       fun closed_at: nullable ISODate do
+               var res = json.get_or_null("closed_at")
+               if res isa String then return new ISODate.from_string(res)
+               return null
+       end
+
+       # TODO link to pull request
+
+       # Full description of the issue.
+       fun body: String  do return json["body"].as(String)
+
+       # List of events on this issue.
+       fun events: Array[IssueEvent] do
+               var res = new Array[IssueEvent]
+               var page = 1
+               var array = api.get("{key}/events?page={page}")
+               if not array isa JsonArray then return res
+               while not array.is_empty do
+                       for obj in array do
+                               if not obj isa JsonObject then continue
+                               res.add new IssueEvent.from_json(api, repo, obj)
+                       end
+                       page += 1
+                       array = api.get("{key}/events?page={page}").as(JsonArray)
+               end
+               return res
+       end
+
+       # User that closed this issue (if any).
+       fun closed_by: nullable User do
+               var closer = json.get_or_null("closed_by")
+               if closer isa JsonObject then return new User.from_json(api, closer)
+               return null
+       end
+end
+
+# A Github pull request.
+#
+# Should be accessed from `GithubAPI::load_pull`.
+#
+# PullRequest are basically Issues with more data.
+# See <https://developer.github.com/v3/pulls/>.
+class PullRequest
+       super Issue
+
+       redef var key is lazy do return "{repo.key}/pulls/{number}"
+
+       # Merge time in ISODate format (if any).
+       fun merged_at: nullable ISODate do
+               var res = json.get_or_null("merged_at")
+               if res isa String then return new ISODate.from_string(res)
+               return null
+       end
+
+       # Merge commit SHA.
+       fun merge_commit_sha: String do return json["merge_commit_sha"].as(String)
+
+       # Count of comments made on the pull request diff.
+       fun review_comments: Int do return json["review_comments"].as(Int)
+
+       # Pull request head (can be a commit SHA or a branch name).
+       fun head: PullRef do
+               var json = json["head"].as(JsonObject)
+               return new PullRef(api, json)
+       end
+
+       # Pull request base (can be a commit SHA or a branch name).
+       fun base: PullRef do
+               var json = json["base"].as(JsonObject)
+               return new PullRef(api, json)
+       end
+
+       # Is this pull request merged?
+       fun merged: Bool do return json["merged"].as(Bool)
+
+       # Is this pull request mergeable?
+       fun mergeable: Bool do return json["mergeable"].as(Bool)
+
+       # Mergeable state of this pull request.
+       #
+       # See <https://developer.github.com/v3/pulls/#list-pull-requests>.
+       fun mergeable_state: Int do return json["mergeable_state"].as(Int)
+
+       # User that merged this pull request (if any).
+       fun merged_by: nullable User do
+               var merger = json.get_or_null("merged_by")
+               if merger isa JsonObject then return new User.from_json(api, merger)
+               return null
+       end
+
+       # Count of commits in this pull request.
+       fun commits: Int do return json["commits"].as(Int)
+
+       # Added line count.
+       fun additions: Int do return json["additions"].as(Int)
+
+       # Deleted line count.
+       fun deletions: Int do return json["deletions"].as(Int)
+
+       # Changed files count.
+       fun changed_files: Int do return json["changed_files"].as(Int)
+end
+
+# A pull request reference (used for head and base).
+class PullRef
+
+       # Api instance that maintains self.
+       var api: GithubAPI
+
+       # JSON representation.
+       var json: JsonObject
+
+       # Label pointed by `self`.
+       fun labl: String do return json["label"].as(String)
+
+       # Reference pointed by `self`.
+       fun ref: String do return json["ref"].as(String)
+
+       # Commit SHA pointed by `self`.
+       fun sha: String do return json["sha"].as(String)
+
+       # User pointed by `self`.
+       fun user: User do
+               return new User.from_json(api, json["user"].as(JsonObject))
+       end
+
+       # Repo pointed by `self`.
+       fun repo: Repo do
+               return new Repo.from_json(api, json["repo"].as(JsonObject))
+       end
 end
 
 # A Github label.
 end
 
 # A Github label.
@@ -508,12 +910,12 @@ class Label
        var name: String
 
        redef init from_json(api, repo, json) do
        var name: String
 
        redef init from_json(api, repo, json) do
-               self.name = json["name"].to_s
+               self.name = json["name"].as(String)
                super
        end
 
        # Label color code.
                super
        end
 
        # Label color code.
-       fun color: String do return json["color"].to_s
+       fun color: String do return json["color"].as(String)
 end
 
 # A Github milestone.
 end
 
 # A Github milestone.
@@ -535,23 +937,23 @@ class Milestone
        end
 
        # Milestone title.
        end
 
        # Milestone title.
-       fun title: String do return json["title"].to_s
+       fun title: String do return json["title"].as(String)
 
        # Milestone long description.
 
        # Milestone long description.
-       fun description: String do return json["description"].to_s
+       fun description: String do return json["description"].as(String)
 
        # Count of opened issues linked to this milestone.
 
        # Count of opened issues linked to this milestone.
-       fun open_issues: Int do return json["open_issues"].to_s.to_i
+       fun open_issues: Int do return json["open_issues"].as(Int)
 
        # Count of closed issues linked to this milestone.
 
        # Count of closed issues linked to this milestone.
-       fun closed_issues: Int do return json["closed_issues"].to_s.to_i
+       fun closed_issues: Int do return json["closed_issues"].as(Int)
 
        # Milestone state.
 
        # Milestone state.
-       fun state: String do return json["state"].to_s
+       fun state: String do return json["state"].as(String)
 
        # Creation time in ISODate format.
        fun created_at: ISODate do
 
        # Creation time in ISODate format.
        fun created_at: ISODate do
-               return new ISODate.from_string(json["created_at"].to_s)
+               return new ISODate.from_string(json["created_at"].as(String))
        end
 
        # User that created this milestone.
        end
 
        # User that created this milestone.
@@ -561,22 +963,288 @@ class Milestone
 
        # Due time in ISODate format (if any).
        fun due_on: nullable ISODate do
 
        # Due time in ISODate format (if any).
        fun due_on: nullable ISODate do
-               var res = json["updated_at"]
-               if res == null then return null
-               return new ISODate.from_string(res.to_s)
+               var res = json.get_or_null("updated_at")
+               if res isa String then return new ISODate.from_string(res)
+               return null
        end
 
        # Update time in ISODate format (if any).
        fun updated_at: nullable ISODate do
        end
 
        # Update time in ISODate format (if any).
        fun updated_at: nullable ISODate do
-               var res = json["updated_at"]
-               if res == null then return null
-               return new ISODate.from_string(res.to_s)
+               var res = json.get_or_null("updated_at")
+               if res isa String then return new ISODate.from_string(res)
+               return null
        end
 
        # Close time in ISODate format (if any).
        fun closed_at: nullable ISODate do
        end
 
        # Close time in ISODate format (if any).
        fun closed_at: nullable ISODate do
-               var res = json["closed_at"]
-               if res == null then return null
-               return new ISODate.from_string(res.to_s)
+               var res = json.get_or_null("closed_at")
+               if res isa String then return new ISODate.from_string(res)
+               return null
+       end
+end
+
+# A Github comment
+#
+# There is two kinds of comments:
+#
+# * `CommitComment` are made on a commit page.
+# * `IssueComment` are made on an issue or pull request page.
+# * `ReviewComment` are made on the diff associated to a pull request.
+abstract class Comment
+       super RepoEntity
+
+       # Identifier of this comment.
+       var id: Int
+
+       redef init from_json(api, repo, json) do
+               self.id = json["id"].as(Int)
+               super
+       end
+
+       # User that made this comment.
+       fun user: User do
+               return new User.from_json(api, json["user"].as(JsonObject))
+       end
+
+       # Creation time in ISODate format.
+       fun created_at: ISODate do
+               return new ISODate.from_string(json["created_at"].as(String))
+       end
+
+       # Last update time in ISODate format (if any).
+       fun updated_at: nullable ISODate do
+               var res = json.get_or_null("updated_at")
+               if res isa String then return new ISODate.from_string(res)
+               return null
+       end
+
+       # Comment body text.
+       fun body: String do return json["body"].as(String)
+
+       # Does the comment contain an acknowledgement (+1)
+       fun is_ack: Bool
+       do
+               return body.has("\\+1\\b".to_re) or body.has(":+1:") or body.has(":shipit:")
+       end
+end
+
+# A comment made on a commit.
+class CommitComment
+       super Comment
+
+       redef var key is lazy do return "{repo.key}/comments/{id}"
+
+       # Commented commit.
+       fun commit: Commit do
+               return api.load_commit(repo, json["commit_id"].as(String)).as(not null)
+       end
+
+       # Position of the comment on the line.
+       fun position: nullable String do
+               var res = json.get_or_null("position")
+               if res isa String then return res
+               return null
+       end
+
+       # Line of the comment.
+       fun line: nullable String do
+               var res = json.get_or_null("line")
+               if res isa String then return res
+               return null
+       end
+
+       # Path of the commented file.
+       fun path: String do return json["path"].as(String)
+end
+
+# Comments made on Github issue and pull request pages.
+#
+# Should be accessed from `GithubAPI::load_issue_comment`.
+#
+# See <https://developer.github.com/v3/issues/comments/>.
+class IssueComment
+       super Comment
+
+       redef var key is lazy do return "{repo.key}/issues/comments/{id}"
+
+       # Issue that contains `self`.
+       fun issue: Issue do
+               var number = issue_url.split("/").last.to_i
+               return api.load_issue(repo, number).as(not null)
        end
        end
+
+       # Link to the issue document on API.
+       fun issue_url: String do return json["issue_url"].as(String)
+end
+
+# Comments made on Github pull request diffs.
+#
+# Should be accessed from `GithubAPI::load_diff_comment`.
+#
+# See <https://developer.github.com/v3/pulls/comments/>.
+class ReviewComment
+       super Comment
+
+       redef var key is lazy do return "{repo.key}/pulls/comments/{id}"
+
+       # Pull request that contains `self`.
+       fun pull: PullRequest do
+               var number = pull_request_url.split("/").last.to_i
+               return api.load_pull(repo, number).as(not null)
+       end
+
+       # Link to the pull request on API.
+       fun pull_request_url: String do return json["pull_request_url"].as(String)
+
+       # Diff hunk.
+       fun diff_hunk: String do return json["diff_hunk"].as(String)
+
+       # Path of commented file.
+       fun path: String do return json["path"].as(String)
+
+       # Position of the comment on the file.
+       fun position: Int do return json["position"].as(Int)
+
+       # Original position in the diff.
+       fun original_position: Int do return json["original_position"].as(Int)
+
+       # Commit referenced by this comment.
+       fun commit_id: String do return json["commit_id"].as(String)
+
+       # Original commit id.
+       fun original_commit_id: String do return json["original_commit_id"].as(String)
+end
+
+# An event that occurs on a Github `Issue`.
+#
+# Should be accessed from `GithubAPI::load_issue_event`.
+#
+# See <https://developer.github.com/v3/issues/events/>.
+class IssueEvent
+       super RepoEntity
+
+       redef var key is lazy do return "{repo.key}/issues/events/{id}"
+
+       # Event id on Github.
+       var id: Int
+
+       redef init from_json(api, repo, json) do
+               self.id = json["id"].as(Int)
+               super
+       end
+
+       # Issue that contains `self`.
+       fun issue: Issue do
+               return new Issue.from_json(api, repo, json["issue"].as(JsonObject))
+       end
+
+       # User that initiated the event.
+       fun actor: User do
+               return new User.from_json(api, json["actor"].as(JsonObject))
+       end
+
+       # Creation time in ISODate format.
+       fun created_at: ISODate do
+               return new ISODate.from_string(json["created_at"].as(String))
+       end
+
+       # Event descriptor.
+       fun event: String do return json["event"].as(String)
+
+       # Commit linked to this event (if any).
+       fun commit_id: nullable String do
+               var res = json.get_or_null("commit_id")
+               if res isa String then return res
+               return null
+       end
+
+       # Label linked to this event (if any).
+       fun labl: nullable Label do
+               var res = json.get_or_null("label")
+               if res isa JsonObject then return new Label.from_json(api, repo, res)
+               return null
+       end
+
+       # User linked to this event (if any).
+       fun assignee: nullable User do
+               var res = json.get_or_null("assignee")
+               if res isa JsonObject then return new User.from_json(api, res)
+               return null
+       end
+
+       # Milestone linked to this event (if any).
+       fun milestone: nullable Milestone do
+               var res = json.get_or_null("milestone")
+               if res isa JsonObject then return new Milestone.from_json(api, repo, res)
+               return null
+       end
+
+       # Rename linked to this event (if any).
+       fun rename: nullable RenameAction do
+               var res = json.get_or_null("rename")
+               if res isa JsonObject then return new RenameAction(res)
+               return null
+       end
+end
+
+# A rename action maintains the name before and after a renaming action.
+class RenameAction
+
+       # JSON content.
+       var json: JsonObject
+
+       # Name before renaming.
+       fun from: String do return json["from"].as(String)
+
+       # Name after renaming.
+       fun to: String do return json["to"].as(String)
+end
+
+# Contributors list with additions, deletions, and commit counts.
+#
+# Should be accessed from `Repo::contrib_stats`.
+#
+# See <https://developer.github.com/v3/repos/statistics/>.
+class ContributorStats
+       super Comparable
+
+       redef type OTHER: ContributorStats
+
+       # Github API client.
+       var api: GithubAPI
+
+       # Json content.
+       var json: JsonObject
+
+       # Init `self` from a `json` object.
+       init from_json(api: GithubAPI, json: JsonObject) do
+               self.api = api
+               self.json = json
+       end
+
+       # User these statistics are about.
+       fun author: User do
+               return new User.from_json(api, json["author"].as(JsonObject))
+       end
+
+       # Total number of commit.
+       fun total: Int do return json["total"].as(Int)
+
+       # Are of weeks of activity with detailed statistics.
+       fun weeks: JsonArray do return json["weeks"].as(JsonArray)
+
+       # ContributorStats can be compared on the total amount of commits.
+       redef fun <(o) do return total < o.total
+end
+
+# A Github file representation.
+#
+# Mostly a wrapper around a json object.
+class GithubFile
+
+       # Json content.
+       var json: JsonObject
+
+       # File name.
+       fun filename: String do return json["filename"].as(String)
 end
 end