lib/github: better type safety on json accesses
[nit.git] / lib / github / api.nit
index ba8943e..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,10 +34,10 @@ 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")
+# var repo = api.load_repo("nitlang/nit")
 # assert repo != null
 # assert repo.name == "nit"
 #
 # assert repo != null
 # assert repo.name == "nit"
 #
@@ -49,9 +47,16 @@ import github_curl
 # ~~~
 class GithubAPI
 
 # ~~~
 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"
        #
@@ -140,9 +145,9 @@ class GithubAPI
                return res.as(JsonObject)
        end
 
                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")
        #
        #     var api = new GithubAPI(get_github_oauth)
        #     var user = api.load_user("Morriar")
@@ -154,12 +159,12 @@ class GithubAPI
 
        # Get the Github repo with `full_name`.
        #
 
        # 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)
@@ -171,7 +176,7 @@ 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")
+       #     var repo = api.load_repo("nitlang/nit")
        #     assert repo != null
        #     var branch = api.load_branch(repo, "master")
        #     assert branch.name == "master"
        #     assert repo != null
        #     var branch = api.load_branch(repo, "master")
        #     assert branch.name == "master"
@@ -186,7 +191,7 @@ 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")
+       #     var repo = api.load_repo("nitlang/nit")
        #     assert repo != null
        #     var commit = api.load_commit(repo, "64ce1f")
        #     assert commit isa Commit
        #     assert repo != null
        #     var commit = api.load_commit(repo, "64ce1f")
        #     assert commit isa Commit
@@ -200,7 +205,7 @@ class GithubAPI
        # Returns `null` if the issue cannot be found.
        #
        #     var api = new GithubAPI(get_github_oauth)
        # Returns `null` if the issue cannot be found.
        #
        #     var api = new GithubAPI(get_github_oauth)
-       #     var repo = api.load_repo("privat/nit")
+       #     var repo = api.load_repo("nitlang/nit")
        #     assert repo != null
        #     var issue = api.load_issue(repo, 1)
        #     assert issue.title == "Doc"
        #     assert repo != null
        #     var issue = api.load_issue(repo, 1)
        #     assert issue.title == "Doc"
@@ -214,7 +219,7 @@ class GithubAPI
        # Returns `null` if the pull request cannot be found.
        #
        #     var api = new GithubAPI(get_github_oauth)
        # Returns `null` if the pull request cannot be found.
        #
        #     var api = new GithubAPI(get_github_oauth)
-       #     var repo = api.load_repo("privat/nit")
+       #     var repo = api.load_repo("nitlang/nit")
        #     assert repo != null
        #     var pull = api.load_pull(repo, 1)
        #     assert pull.title == "Doc"
        #     assert repo != null
        #     var pull = api.load_pull(repo, 1)
        #     assert pull.title == "Doc"
@@ -229,7 +234,7 @@ 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")
+       #     var repo = api.load_repo("nitlang/nit")
        #     assert repo != null
        #     var labl = api.load_label(repo, "ok_will_merge")
        #     assert labl != null
        #     assert repo != null
        #     var labl = api.load_label(repo, "ok_will_merge")
        #     assert labl != null
@@ -243,7 +248,7 @@ 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")
+       #     var repo = api.load_repo("nitlang/nit")
        #     assert repo != null
        #     var stone = api.load_milestone(repo, 4)
        #     assert stone.title == "v1.0prealpha"
        #     assert repo != null
        #     var stone = api.load_milestone(repo, 4)
        #     assert stone.title == "v1.0prealpha"
@@ -257,7 +262,7 @@ class GithubAPI
        # Returns `null` if the event cannot be found.
        #
        #     var api = new GithubAPI(get_github_oauth)
        # Returns `null` if the event cannot be found.
        #
        #     var api = new GithubAPI(get_github_oauth)
-       #     var repo = api.load_repo("privat/nit")
+       #     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 repo isa Repo
        #     var event = api.load_issue_event(repo, 199674194)
        #     assert event.actor.login == "privat"
@@ -274,7 +279,7 @@ class GithubAPI
        # Returns `null` if the comment cannot be found.
        #
        #     var api = new GithubAPI(get_github_oauth)
        # Returns `null` if the comment cannot be found.
        #
        #     var api = new GithubAPI(get_github_oauth)
-       #     var repo = api.load_repo("privat/nit")
+       #     var repo = api.load_repo("nitlang/nit")
        #     assert repo != null
        #     var comment = api.load_commit_comment(repo, 8982707)
        #     assert comment.user.login == "Morriar"
        #     assert repo != null
        #     var comment = api.load_commit_comment(repo, 8982707)
        #     assert comment.user.login == "Morriar"
@@ -290,7 +295,7 @@ class GithubAPI
        # Returns `null` if the comment cannot be found.
        #
        #     var api = new GithubAPI(get_github_oauth)
        # Returns `null` if the comment cannot be found.
        #
        #     var api = new GithubAPI(get_github_oauth)
-       #     var repo = api.load_repo("privat/nit")
+       #     var repo = api.load_repo("nitlang/nit")
        #     assert repo != null
        #     var comment = api.load_issue_comment(repo, 6020149)
        #     assert comment.user.login == "privat"
        #     assert repo != null
        #     var comment = api.load_issue_comment(repo, 6020149)
        #     assert comment.user.login == "privat"
@@ -306,7 +311,7 @@ class GithubAPI
        # Returns `null` if the comment cannot be found.
        #
        #     var api = new GithubAPI(get_github_oauth)
        # Returns `null` if the comment cannot be found.
        #
        #     var api = new GithubAPI(get_github_oauth)
-       #     var repo = api.load_repo("privat/nit")
+       #     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 repo != null
        #     var comment = api.load_review_comment(repo, 21010363)
        #     assert comment.path == "src/modelize/modelize_property.nit"
@@ -344,13 +349,15 @@ abstract class GithubEntity
        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
 
@@ -361,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
 
@@ -387,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
@@ -410,7 +410,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 Branch.from_json(api, self, obj)
                end
                return res
                        res[name] = new Branch.from_json(api, self, obj)
                end
                return res
@@ -431,6 +431,27 @@ class Repo
                return res
        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")
        # Get the last published issue.
        fun last_issue: nullable Issue do
                var array = api.get("repos/{full_name}/issues")
@@ -449,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
@@ -507,7 +528,7 @@ class Repo
 
        # 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
@@ -543,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
 
@@ -578,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
 
@@ -588,56 +609,54 @@ 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]
        end
 
        # List files staged in this commit.
        fun files: Array[GithubFile] do
                var res = new Array[GithubFile]
-               var files = json["files"]
+               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)))
                if not files isa JsonArray then return res
                for obj in files do
                        res.add(new GithubFile(obj.as(JsonObject)))
@@ -646,7 +665,7 @@ class Commit
        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.
 end
 
 # A Github issue.
@@ -668,7 +687,7 @@ class Issue
        end
 
        # Issue title.
        end
 
        # Issue title.
-       fun title: String do return json["title"].to_s
+       fun title: String do return json["title"].as(String)
 
        # User that created this issue.
        fun user: User do
 
        # User that created this issue.
        fun user: User do
@@ -678,33 +697,34 @@ class Issue
        # List of labels on this issue associated to their names.
        fun labels: Map[String, Label] do
                var res = new HashMap[String, Label]
        # List of labels on this issue associated to their names.
        fun labels: Map[String, Label] do
                var res = new HashMap[String, Label]
-               if not json.has_key("labels") then return res
-               for obj in json["labels"].as(JsonArray) do
+               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
                        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, repo, obj)
                end
                return res
        end
 
        # State of the issue on Github.
                        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"].to_s
+       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
 
        # 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["assignee"]
-               if not assignee isa JsonObject then return null
-               return new User.from_json(api, assignee)
+               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
        end
 
        # `Milestone` (if any).
        fun milestone: nullable Milestone do
-               var milestone = json["milestone"]
-               if not milestone isa JsonObject then return null
-               return new Milestone.from_json(api, repo, milestone)
+               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.
        end
 
        # List of comments made on this issue.
@@ -729,37 +749,38 @@ class Issue
        end
 
        # Number of comments on this issue.
        end
 
        # Number of comments on this issue.
-       fun comments_count: Int do return json["comments"].to_s.to_i
+       fun comments_count: Int do return json["comments"].as(Int)
 
        # 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
 
        # Last update time in ISODate format (if any).
        fun updated_at: nullable ISODate do
        end
 
        # Last 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
 
        # TODO link to pull request
 
        # Full description of the issue.
        end
 
        # TODO link to pull request
 
        # Full description of the issue.
-       fun body: String  do return json["body"].to_s
+       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
 
        # 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}").as(JsonArray)
+               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
                while not array.is_empty do
                        for obj in array do
                                if not obj isa JsonObject then continue
@@ -773,9 +794,9 @@ class Issue
 
        # User that closed this issue (if any).
        fun closed_by: nullable User do
 
        # User that closed this issue (if any).
        fun closed_by: nullable User do
-               var closer = json["closed_by"]
-               if not closer isa JsonObject then return null
-               return new User.from_json(api, closer)
+               var closer = json.get_or_null("closed_by")
+               if closer isa JsonObject then return new User.from_json(api, closer)
+               return null
        end
 end
 
        end
 end
 
@@ -792,16 +813,16 @@ class PullRequest
 
        # Merge time in ISODate format (if any).
        fun merged_at: nullable ISODate do
 
        # Merge time in ISODate format (if any).
        fun merged_at: nullable ISODate do
-               var res = json["merged_at"]
-               if res == null then return null
-               return new ISODate.from_string(res.to_s)
+               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.
        end
 
        # Merge commit SHA.
-       fun merge_commit_sha: String do return json["merge_commit_sha"].to_s
+       fun merge_commit_sha: String do return json["merge_commit_sha"].as(String)
 
        # Count of comments made on the pull request diff.
 
        # Count of comments made on the pull request diff.
-       fun review_comments: Int do return json["review_comments"].to_s.to_i
+       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
 
        # Pull request head (can be a commit SHA or a branch name).
        fun head: PullRef do
@@ -824,26 +845,26 @@ class PullRequest
        # Mergeable state of this pull request.
        #
        # See <https://developer.github.com/v3/pulls/#list-pull-requests>.
        # 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"].to_s.to_i
+       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
 
        # User that merged this pull request (if any).
        fun merged_by: nullable User do
-               var merger = json["merged_by"]
-               if not merger isa JsonObject then return null
-               return new User.from_json(api, merger)
+               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.
        end
 
        # Count of commits in this pull request.
-       fun commits: Int do return json["commits"].to_s.to_i
+       fun commits: Int do return json["commits"].as(Int)
 
        # Added line count.
 
        # Added line count.
-       fun additions: Int do return json["additions"].to_s.to_i
+       fun additions: Int do return json["additions"].as(Int)
 
        # Deleted line count.
 
        # Deleted line count.
-       fun deletions: Int do return json["deletions"].to_s.to_i
+       fun deletions: Int do return json["deletions"].as(Int)
 
        # Changed files count.
 
        # Changed files count.
-       fun changed_files: Int do return json["changed_files"].to_s.to_i
+       fun changed_files: Int do return json["changed_files"].as(Int)
 end
 
 # A pull request reference (used for head and base).
 end
 
 # A pull request reference (used for head and base).
@@ -856,13 +877,13 @@ class PullRef
        var json: JsonObject
 
        # Label pointed by `self`.
        var json: JsonObject
 
        # Label pointed by `self`.
-       fun labl: String do return json["label"].to_s
+       fun labl: String do return json["label"].as(String)
 
        # Reference pointed by `self`.
 
        # Reference pointed by `self`.
-       fun ref: String do return json["ref"].to_s
+       fun ref: String do return json["ref"].as(String)
 
        # Commit SHA pointed by `self`.
 
        # Commit SHA pointed by `self`.
-       fun sha: String do return json["sha"].to_s
+       fun sha: String do return json["sha"].as(String)
 
        # User pointed by `self`.
        fun user: User do
 
        # User pointed by `self`.
        fun user: User do
@@ -889,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.
@@ -916,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.
@@ -942,23 +963,23 @@ 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
 
        end
 end
 
@@ -987,17 +1008,24 @@ abstract class Comment
 
        # 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
 
        # Last update time in ISODate format (if any).
        fun updated_at: nullable ISODate do
        end
 
        # Last update time in ISODate format (if any).
        fun updated_at: nullable ISODate do
-               if not json.has_key("updated_at") then return null
-               return new ISODate.from_string(json["updated_at"].to_s)
+               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.
        end
 
        # Comment body text.
-       fun body: String do return json["body"].to_s
+       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.
 end
 
 # A comment made on a commit.
@@ -1008,27 +1036,25 @@ class CommitComment
 
        # Commented commit.
        fun commit: Commit do
 
        # Commented commit.
        fun commit: Commit do
-               return api.load_commit(repo, json["commit_id"].to_s).as(not null)
+               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
        end
 
        # Position of the comment on the line.
        fun position: nullable String do
-               if not json.has_key("position") then return null
-               var res = json["position"]
-               if res == null then return null
-               return res.to_s
+               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
        end
 
        # Line of the comment.
        fun line: nullable String do
-               if not json.has_key("line") then return null
-               var res = json["line"]
-               if res == null then return null
-               return res.to_s
+               var res = json.get_or_null("line")
+               if res isa String then return res
+               return null
        end
 
        # Path of the commented file.
        end
 
        # Path of the commented file.
-       fun path: String do return json["path"].to_s
+       fun path: String do return json["path"].as(String)
 end
 
 # Comments made on Github issue and pull request pages.
 end
 
 # Comments made on Github issue and pull request pages.
@@ -1048,7 +1074,7 @@ class IssueComment
        end
 
        # Link to the issue document on API.
        end
 
        # Link to the issue document on API.
-       fun issue_url: String do return json["issue_url"].to_s
+       fun issue_url: String do return json["issue_url"].as(String)
 end
 
 # Comments made on Github pull request diffs.
 end
 
 # Comments made on Github pull request diffs.
@@ -1068,25 +1094,25 @@ class ReviewComment
        end
 
        # Link to the pull request on API.
        end
 
        # Link to the pull request on API.
-       fun pull_request_url: String do return json["pull_request_url"].to_s
+       fun pull_request_url: String do return json["pull_request_url"].as(String)
 
        # Diff hunk.
 
        # Diff hunk.
-       fun diff_hunk: String do return json["diff_hunk"].to_s
+       fun diff_hunk: String do return json["diff_hunk"].as(String)
 
        # Path of commented file.
 
        # Path of commented file.
-       fun path: String do return json["path"].to_s
+       fun path: String do return json["path"].as(String)
 
        # Position of the comment on the file.
 
        # Position of the comment on the file.
-       fun position: Int do return json["position"].to_s.to_i
+       fun position: Int do return json["position"].as(Int)
 
        # Original position in the diff.
 
        # Original position in the diff.
-       fun original_position: Int do return json["original_position"].to_s.to_i
+       fun original_position: Int do return json["original_position"].as(Int)
 
        # Commit referenced by this comment.
 
        # Commit referenced by this comment.
-       fun commit_id: String do return json["commit_id"].to_s
+       fun commit_id: String do return json["commit_id"].as(String)
 
        # Original commit id.
 
        # Original commit id.
-       fun original_commit_id: String do return json["original_commit_id"].to_s
+       fun original_commit_id: String do return json["original_commit_id"].as(String)
 end
 
 # An event that occurs on a Github `Issue`.
 end
 
 # An event that occurs on a Github `Issue`.
@@ -1119,45 +1145,45 @@ class IssueEvent
 
        # 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
 
        # Event descriptor.
        end
 
        # Event descriptor.
-       fun event: String do return json["event"].to_s
+       fun event: String do return json["event"].as(String)
 
        # Commit linked to this event (if any).
        fun commit_id: nullable String do
 
        # Commit linked to this event (if any).
        fun commit_id: nullable String do
-               var res = json["commit_id"]
-               if res == null then return null
-               return res.to_s
+               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
        end
 
        # Label linked to this event (if any).
        fun labl: nullable Label do
-               var res = json["label"]
-               if not res isa JsonObject then return null
-               return new Label.from_json(api, repo, res)
+               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
        end
 
        # User linked to this event (if any).
        fun assignee: nullable User do
-               var res = json["assignee"]
-               if not res isa JsonObject then return null
-               return new User.from_json(api, res)
+               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
        end
 
        # Milestone linked to this event (if any).
        fun milestone: nullable Milestone do
-               var res = json["milestone"]
-               if not res isa JsonObject then return null
-               return new Milestone.from_json(api, repo, res)
+               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
        end
 
        # Rename linked to this event (if any).
        fun rename: nullable RenameAction do
-               var res = json["rename"]
-               if res == null then return null
-               return new RenameAction(res.as(JsonObject))
+               var res = json.get_or_null("rename")
+               if res isa JsonObject then return new RenameAction(res)
+               return null
        end
 end
 
        end
 end
 
@@ -1168,10 +1194,10 @@ class RenameAction
        var json: JsonObject
 
        # Name before renaming.
        var json: JsonObject
 
        # Name before renaming.
-       fun from: String do return json["from"].to_s
+       fun from: String do return json["from"].as(String)
 
        # Name after renaming.
 
        # Name after renaming.
-       fun to: String do return json["to"].to_s
+       fun to: String do return json["to"].as(String)
 end
 
 # Contributors list with additions, deletions, and commit counts.
 end
 
 # Contributors list with additions, deletions, and commit counts.
@@ -1202,7 +1228,7 @@ class ContributorStats
        end
 
        # Total number of commit.
        end
 
        # Total number of commit.
-       fun total: Int do return json["total"].to_s.to_i
+       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)
 
        # Are of weeks of activity with detailed statistics.
        fun weeks: JsonArray do return json["weeks"].as(JsonArray)
@@ -1220,5 +1246,5 @@ class GithubFile
        var json: JsonObject
 
        # File name.
        var json: JsonObject
 
        # File name.
-       fun filename: String do return json["filename"].to_s
+       fun filename: String do return json["filename"].as(String)
 end
 end