Merge: json::serialization: fallback to the static type when there is no metadata...
authorJean Privat <jean@pryen.org>
Mon, 15 Aug 2016 19:16:59 +0000 (15:16 -0400)
committerJean Privat <jean@pryen.org>
Mon, 15 Aug 2016 19:16:59 +0000 (15:16 -0400)
Move and improve some operations between two of the serialization phases, placing them after `modelize_property_phase` so they have access to the static type of serializable attributes.

The static type is then used by the JSON deserializer as a fallback if there is no metadata and no heuristic to find which Nit type to deserialize. This affects both the attributes of standard serializable classes and the items of `SimpleCollection`, when using the `JsonDeserializer` only. This means the we need much less `class_name_heuristic` for reading plain JSON.

The static types of generic attributes is also used as heuristic to support deserializing the given parameterized type. So as long as the static type is not an abstract class, does not need an anchor, is not a subclass hiding the parameter E (for JSON arrays)... it should work. This is actually most of the time, so avoid abstract generic static types like `Set` or `Sequence`. This also means less `class_name_heuristic` for any JSON and binary data.

All of it makes it easier to deal with the following input `json_string` without metadata:
~~~
var plain_json = """{
    "corners": [{"x": 0, "y": 0},
                {"x": 3, "y": 0},
                {"x": 2, "y": 2}],
    "name": "the same triangle"
}"""
~~~

which can be deserialized with the following Nit code:
~~~
module my_module is serialize

class Triangle
    var corners = new Array[Point] # Could as well be HashSet[Point]
    redef var to_s is serialize_as("name") # The static type comes from the model
end

class Point
    var x: Int
    var y: Int
end

var deser_engine = new JsonDeserializer(json_string)
var obj = new Triangle.deserialize_from(deser_engine) # The root object still needs a type
assert deser_engine.errors.is_empty # If false, don't trust `obj`

print obj
print obj.other_corners
~~~

Pull-Request: #2257
Reviewed-by: Jean Privat <jean@pryen.org>
Reviewed-by: Alexandre Terrasa <alexandre@moz-code.org>

26 files changed:
contrib/nitrpg/src/achievements.nit
contrib/nitrpg/src/events_generator.nit
contrib/nitrpg/src/reactors.nit
contrib/nitrpg/src/templates/panels.nit
contrib/nitrpg/src/templates/templates_base.nit
contrib/nitrpg/src/templates/templates_events.nit
contrib/nitrpg/src/test_listener.nit
lib/github/api.nit
lib/github/cache.nit
lib/github/events.nit
lib/github/hooks.nit
lib/mongodb/queries.nit [new file with mode: 0644]
lib/nitcorn/http_request.nit
lib/nitcorn/http_response.nit
lib/popcorn/pop_auth.nit
lib/popcorn/pop_repos.nit
lib/trees/trie.nit [new file with mode: 0644]
share/man/nitls.md
share/nitweb/directives/user/user-menu.html [new file with mode: 0644]
share/nitweb/index.html
share/nitweb/javascripts/model.js
share/nitweb/javascripts/nitweb.js
share/nitweb/javascripts/users.js [new file with mode: 0644]
share/nitweb/stylesheets/nitweb.css
share/nitweb/views/user.html [new file with mode: 0644]
src/nitweb.nit

index 24fac20..9055b22 100644 (file)
@@ -156,7 +156,7 @@ redef class Player
                obj["player"] = name
                obj["reward"] = achievement.reward
                obj["achievement"] = achievement.id
-               obj["github_event"] = event.json
+               obj["github_event"] = event
                var ge = new GameEvent(game, "achievement_unlocked", obj)
                add_event(ge)
                game.add_event(ge)
index cc8b6a4..9e5bf30 100644 (file)
@@ -29,66 +29,65 @@ class EventsGenerator
        # API client used to get github data.
        var api: GithubAPI
 
+       # Gen a fake id for events
+       fun gen_event_id: String do return get_time.to_s
+
        # Issues
 
        # Generate a new IssuesEvent from an issue.
-       fun issues_event(action: String, issue: Issue): IssuesEvent do
-               var e = new IssuesEvent(api)
-               e.action = action
-               e.repo = issue.repo
-               e.issue = issue
-               return e
+       fun issues_event(repo: Repo, action: String, issue: Issue): IssuesEvent do
+               return new IssuesEvent(gen_event_id, action, repo, issue)
        end
 
        # Generate a new IssuesEvent with an `opened` action.
-       fun issue_open(issue: Issue): IssuesEvent do return issues_event("opened", issue)
+       fun issue_open(repo: Repo, issue: Issue): IssuesEvent do
+               return issues_event(repo, "opened", issue)
+       end
 
        # Generate a new IssuesEvent with an `closed` action.
-       fun issue_close(issue: Issue): IssuesEvent do return issues_event("closed", issue)
+       fun issue_close(repo: Repo, issue: Issue): IssuesEvent do
+               return issues_event(repo, "closed", issue)
+       end
 
        # Generate a new IssuesEvent with an `reopened` action.
-       fun issue_reopen(issue: Issue): IssuesEvent do return issues_event("reopened", issue)
+       fun issue_reopen(repo: Repo, issue: Issue): IssuesEvent do
+               return issues_event(repo, "reopened", issue)
+       end
 
        # Generate a new IssuesEvent from a IssueEvent.
-       fun issue_raw_event(issue: Issue, event: IssueEvent): IssuesEvent do
-               var e = issues_event(event.event, issue)
-               e.lbl = event.labl
-               e.assignee = event.assignee
-               return e
+       fun issue_raw_event(repo: Repo, issue: Issue, event: IssueEvent): IssuesEvent do
+               return new IssuesEvent(event.id.to_s, event.event, repo, issue, event.labl, event.assignee)
+       end
+
+       # Generate a new IssueCommentEvent from a IssueComment.
+       fun issue_comment_event(repo: Repo, issue: Issue, comment: IssueComment): IssueCommentEvent do
+               return new IssueCommentEvent(gen_event_id, "created", repo, issue, comment)
        end
 
        # Pull requests
 
        # Generate a new PullRequestEvent from a `pull` request.
-       fun pull_event(action: String, pull: PullRequest): PullRequestEvent do
-               var e = new PullRequestEvent(api)
-               e.action = action
-               e.repo = pull.repo
-               e.pull = pull
-               return e
+       fun pull_event(repo: Repo, action: String, pull: PullRequest): PullRequestEvent do
+               return new PullRequestEvent(gen_event_id, action, repo, pull.number, pull)
        end
 
        # Generate a new PullRequestEvent with an `opened` action.
-       fun pull_open(pull: PullRequest): PullRequestEvent do return pull_event("opened", pull)
+       fun pull_open(repo: Repo, pull: PullRequest): PullRequestEvent do
+               return pull_event(repo, "opened", pull)
+       end
 
        # Generate a new PullRequestEvent with an `closed` action.
-       fun pull_close(pull: PullRequest): PullRequestEvent do return pull_event("closed", pull)
+       fun pull_close(repo: Repo, pull: PullRequest): PullRequestEvent do
+               return pull_event(repo, "closed", pull)
+       end
 
        # Generate a new PullRequestEvent with an `reopened` action.
-       fun pull_reopen(pull: PullRequest): PullRequestEvent do return pull_event("reopened", pull)
-
-       # Generate a new PullRequestEvent from a IssueEvent.
-       fun pull_raw_event(pull: PullRequest, event: IssueEvent): PullRequestEvent do
-               return pull_event(event.event, pull)
+       fun pull_reopen(repo: Repo, pull: PullRequest): PullRequestEvent do
+               return pull_event(repo, "reopened", pull)
        end
 
-       # Generate a new IssueCommentEvent from a IssueComment.
-       fun issue_comment_event(issue: Issue, comment: IssueComment): IssueCommentEvent do
-               var e = new IssueCommentEvent(api)
-               e.action = "created"
-               e.repo = issue.repo
-               e.issue = issue
-               e.comment = comment
-               return e
+       # Generate a new PullRequestEvent from a IssueEvent.
+       fun pull_raw_event(repo: Repo, pull: PullRequest, event: IssueEvent): PullRequestEvent do
+               return new PullRequestEvent(event.id.to_s, event.event, repo, pull.number, pull)
        end
 end
index e2717f4..616bd8a 100644 (file)
@@ -47,7 +47,7 @@ redef class GithubEvent
                var obj = new JsonObject
                obj["player"] = player.name
                obj["reward"] = reward
-               obj["github_event"] = json
+               obj["github_event"] = self
                var event = new GameEvent(player.game, kind, obj)
                player.game.add_event(event)
                return event
index 6d5e339..8f27a5f 100644 (file)
@@ -217,7 +217,7 @@ class PlayerStatusPanel
        redef fun render_title do
                add "<a href=\"{player.url}\">"
                add " <img class=\"img-circle\" style=\"width: 30px\""
-               add "   src=\"{player.user.avatar_url}\" alt=\"{player.name}\">"
+               add "   src=\"{player.user.avatar_url or else "#"}\" alt=\"{player.name}\">"
                add "</a>&nbsp;&nbsp;{player.link}"
        end
 
@@ -333,7 +333,7 @@ class PodiumPanel
                                        <p>
                                                <a href="{{{player.url}}}">
                                                        <img class="img-circle" style="width: 80px"
-                                                               src="{{{player.user.avatar_url}}}" alt="{{{player.name}}}">
+                                                               src="{{{player.user.avatar_url or else "#"}}}" alt="{{{player.name}}}">
                                                </a>
                                        </p>
                                        <p>{{{player.link}}}</p>
@@ -371,10 +371,8 @@ class PlayerReviewsPanel
                        "-involves:{player.name}"
 
                var issues = new ArraySet[Issue]
-               var rq = game.repo.search_issues(q)
-               if rq != null then issues.add_all rq
-               var rq2 = game.repo.search_issues(q2)
-               if rq2 != null then issues.add_all rq2
+               issues.add_all game.api.search_repo_issues(game.repo, q)
+               issues.add_all game.api.search_repo_issues(game.repo, q2)
                if issues.is_empty then
                        add "<em>No pull request or issue to review yet...</em>"
                        return
@@ -385,7 +383,7 @@ class PlayerReviewsPanel
                        add """<div class="media">
                                <a class="media-left" href="{{{uplay.url}}}">
                                         <img class=\"img-circle\" style="width:50px"
-                                          src="{{{user.avatar_url}}}" alt="{{{uplay.name}}}">
+                                                src="{{{user.avatar_url or else "#"}}}" alt="{{{uplay.name}}}">
                                        </a>
                                        <div class="media-body">
                                         <h4 class="media-heading">
@@ -419,10 +417,8 @@ class PlayerWorkPanel
                var q2 = "is:open sort:updated-asc assignee:{player.name}"
 
                var issues = new ArraySet[Issue]
-               var rq = game.repo.search_issues(q)
-               if rq != null then issues.add_all rq
-               var rq2 = game.repo.search_issues(q2)
-               if rq2 != null then issues.add_all rq2
+               issues.add_all game.api.search_repo_issues(game.repo, q)
+               issues.add_all game.api.search_repo_issues(game.repo, q2)
                if issues.is_empty then
                        add "<em>No work to do yet...</em>"
                        return
@@ -433,7 +429,7 @@ class PlayerWorkPanel
                        add """<div class="media">
                                <a class="media-left" href="{{{uplay.url}}}">
                                         <img class=\"img-circle\" style="width:50px"
-                                          src="{{{user.avatar_url}}}" alt="{{{uplay.name}}}">
+                                                src="{{{user.avatar_url or else "#"}}}" alt="{{{uplay.name}}}">
                                        </a>
                                        <div class="media-body">
                                         <h4 class="media-heading">
@@ -557,7 +553,7 @@ class AchievementPanel
                        <a class="media-left" href="{{{player.url}}}">
                                 <span class="badge progress-bar-warning" style="position: absolute">#1</span>
                         <img class=\"img-circle\" style="width:50px"
-                         src="{{{player.user.avatar_url}}}" alt="{{{player.name}}}">
+                                        src="{{{player.user.avatar_url or else "#"}}}" alt="{{{player.name}}}">
                        </a>
                                <div class="media-body">
                                 <h4 class="media-heading">Unlocked first by {{{player.link}}}</h4>
index 470160b..fe9efd8 100644 (file)
@@ -48,13 +48,14 @@ end
 
 redef class Issue
        # Return a HTML link to this Issue.
-       fun link: String do return "<a href=\"{html_url}\">#{number}</a>"
+       fun link: String do return "<a href=\"{html_url or else "#"}\">#{number}</a>"
 end
 
 redef class Achievement
        # Return a HTML link to this Issue.
        fun link: String do return "<a href=\"{url}\">{name}</a>"
 
+       # Render self as a media item.
        fun list_item: String do
                return """<div class="media">
                               <div class="media-left" style="width: 50px">
index 234c9e5..3e141b9 100644 (file)
@@ -55,14 +55,12 @@ class TplEvent
 
        # Load `github_event` data key as a PullRequestEvent.
        var pull_event: PullRequestEvent is lazy do
-               var obj = event.data["github_event"].as(JsonObject)
-               return new PullRequestEvent.from_json(event.game.api, obj)
+               return event.game.api.deserialize(event.data["github_event"].as(JsonObject).to_json).as(PullRequestEvent)
        end
 
        # Load `github_event` data key as a IssueCommentEvent.
        var issue_comment_event: IssueCommentEvent is lazy do
-               var obj = event.data["github_event"].as(JsonObject)
-               return new IssueCommentEvent.from_json(event.game.api, obj)
+               return event.game.api.deserialize(event.data["github_event"].as(JsonObject).to_json).as(IssueCommentEvent)
        end
 
        # Load `achievement` data key as an Achievement.
@@ -77,7 +75,7 @@ class TplEvent
                                 <span class="badge progress-bar-success"
                                  style=\"position: absolute\">+{{{reward}}}</span>
                                 <img class=\"img-circle\" style="width:50px"
-                                  src="{{{player.user.avatar_url}}}" alt="{{{player.name}}}">
+                                src="{{{player.user.avatar_url or else "#"}}}" alt="{{{player.name}}}">
                                </a>
                                <div class="media-body">
                                 <h4 class="media-heading">{{{title}}}</h4>
index ee6d13b..9cfa059 100644 (file)
@@ -52,15 +52,15 @@ class TestListener
                var issue = api.load_issue(repo, 322)
                assert issue != null
 
-               l.apply_event(generator.issue_open(issue), db)
+               l.apply_event(generator.issue_open(repo, issue), db)
                var game = load_game("Morriar/nit", db)
                assert game.stats.overall["issues"] == 1
                assert game.stats.overall["issues_open"] == 1
-               l.apply_event(generator.issue_close(issue), db)
+               l.apply_event(generator.issue_close(repo, issue), db)
                game = load_game("Morriar/nit", db)
                assert game.stats.overall["issues"] == 1
                assert game.stats.overall["issues_open"] == 0
-               l.apply_event(generator.issue_reopen(issue), db)
+               l.apply_event(generator.issue_reopen(repo, issue), db)
                game = load_game("Morriar/nit", db)
                assert game.stats.overall["issues"] == 1
                assert game.stats.overall["issues_open"] == 1
@@ -75,15 +75,15 @@ class TestListener
                var issue = api.load_issue(repo, 322)
                assert issue != null
 
-               l.apply_event(generator.issue_open(issue), db)
+               l.apply_event(generator.issue_open(repo, issue), db)
                var player = new Player(game, "Morriar")
                assert player.stats.overall["issues"] == 1
                assert player.stats.overall["issues_open"] == 1
-               l.apply_event(generator.issue_close(issue), db)
+               l.apply_event(generator.issue_close(repo, issue), db)
                player = new Player(game, "Morriar")
                assert player.stats.overall["issues"] == 1
                assert player.stats.overall["issues_open"] == 0
-               l.apply_event(generator.issue_reopen(issue), db)
+               l.apply_event(generator.issue_reopen(repo, issue), db)
                player = new Player(game, "Morriar")
                assert player.stats.overall["issues"] == 1
                assert player.stats.overall["issues_open"] == 1
@@ -97,24 +97,24 @@ class TestListener
                var pr = api.load_pull(repo, 275)
                assert pr != null
 
-               l.apply_event(generator.pull_open(pr), db)
+               l.apply_event(generator.pull_open(repo, pr), db)
                var game = load_game("Morriar/nit", db)
                assert game.stats.overall["pulls"] == 1
                assert game.stats.overall["pulls_open"] == 1
                assert game.stats.overall["commits"] == 0
                pr.merged = false
-               l.apply_event(generator.pull_close(pr), db)
+               l.apply_event(generator.pull_close(repo, pr), db)
                game = load_game("Morriar/nit", db)
                assert game.stats.overall["pulls"] == 1
                assert game.stats.overall["pulls_open"] == 0
                assert game.stats.overall["commits"] == 0
-               l.apply_event(generator.pull_reopen(pr), db)
+               l.apply_event(generator.pull_reopen(repo, pr), db)
                game = load_game("Morriar/nit", db)
                assert game.stats.overall["pulls"] == 1
                assert game.stats.overall["pulls_open"] == 1
                assert game.stats.overall["commits"] == 0
                pr.merged = true
-               l.apply_event(generator.pull_close(pr), db)
+               l.apply_event(generator.pull_close(repo, pr), db)
                game = load_game("Morriar/nit", db)
                assert game.stats.overall["pulls"] == 1
                assert game.stats.overall["pulls_open"] == 0
@@ -132,12 +132,12 @@ class TestListener
                assert comment != null
 
                comment.body = "foo bar"
-               l.apply_event(generator.issue_comment_event(issue, comment), db)
+               l.apply_event(generator.issue_comment_event(repo, issue, comment), db)
                var game = load_game("Morriar/nit", db)
                assert game.stats.overall["comments"] == 1
                assert game.stats.overall["reviews"] == 0
                comment.body = "foo +1 bar"
-               l.apply_event(generator.issue_comment_event(issue, comment), db)
+               l.apply_event(generator.issue_comment_event(repo, issue, comment), db)
                game = load_game("Morriar/nit", db)
                assert game.stats.overall["comments"] == 2
                assert game.stats.overall["reviews"] == 1
@@ -152,18 +152,18 @@ class TestListener
                var pull = api.load_pull(repo, 275)
                assert pull != null
 
-               l.apply_event(generator.pull_open(pull), db)
+               l.apply_event(generator.pull_open(repo, pull), db)
                var player = new Player(game, "itch76")
                assert player.stats.overall["nitcoins"] == 10
                pull.merged = false
-               l.apply_event(generator.pull_close(pull), db)
+               l.apply_event(generator.pull_close(repo, pull), db)
                player = new Player(game, "itch76")
                assert player.stats.overall["nitcoins"] == 0
-               l.apply_event(generator.pull_reopen(pull), db)
+               l.apply_event(generator.pull_reopen(repo, pull), db)
                player = new Player(game, "itch76")
                assert player.stats.overall["nitcoins"] == 10
                pull.merged = true
-               l.apply_event(generator.pull_close(pull), db)
+               l.apply_event(generator.pull_close(repo, pull), db)
                player = new Player(game, "itch76")
                assert player.stats.overall["nitcoins"] == 12
        end
@@ -184,14 +184,14 @@ class TestListener
                # no review in opened issue
                pull.state = "open"
                comment.body = "foo bar"
-               l.apply_event(generator.issue_comment_event(pull, comment), db)
+               l.apply_event(generator.issue_comment_event(repo, pull, comment), db)
                var player = new Player(game, "Morriar")
                assert player.stats.overall["nitcoins"] == 0
 
                # review in opened issue
                pull.state = "open"
                comment.body = "foo +1 bar"
-               l.apply_event(generator.issue_comment_event(pull, comment), db)
+               l.apply_event(generator.issue_comment_event(repo, pull, comment), db)
                player = new Player(game, "Morriar")
                print player.stats.overall["nitcoins"]
                assert player.stats.overall["nitcoins"] == 2
@@ -199,14 +199,14 @@ class TestListener
                # review in closed issue
                pull.state = "closed"
                comment.body = "foo +1 bar"
-               l.apply_event(generator.issue_comment_event(pull, comment), db)
+               l.apply_event(generator.issue_comment_event(repo, pull, comment), db)
                player = new Player(game, "Morriar")
                assert player.stats.overall["nitcoins"] == 2
 
                # review in reopened issue
                pull.state = "open"
                comment.body = "foo +1 bar"
-               l.apply_event(generator.issue_comment_event(pull, comment), db)
+               l.apply_event(generator.issue_comment_event(repo, pull, comment), db)
                player = new Player(game, "Morriar")
                assert player.stats.overall["nitcoins"] == 4
        end
@@ -227,7 +227,7 @@ class TestListener
                        var player = new Player(game, "Morriar")
                        player.stats["issues"] = i
                        player.save
-                       l.apply_event(generator.issue_open(issue), db)
+                       l.apply_event(generator.issue_open(repo, issue), db)
                        assert player.load_achievements.has_key(id)
                end
                var player = new Player(game, "Morriar")
@@ -250,7 +250,7 @@ class TestListener
                        var player = new Player(game, "itch76")
                        player.stats["pulls"] = i
                        player.save
-                       l.apply_event(generator.pull_open(pull), db)
+                       l.apply_event(generator.pull_open(repo, pull), db)
                        assert player.load_achievements.has_key(id)
                end
                var player = new Player(game, "itch76")
@@ -276,7 +276,7 @@ class TestListener
                        var player = new Player(game, "itch76")
                        player.stats["commits"] = i
                        player.save
-                       l.apply_event(generator.pull_close(pull), db)
+                       l.apply_event(generator.pull_close(repo, pull), db)
                        assert player.load_achievements.has_key(id)
                end
                var player = new Player(game, "itch76")
@@ -301,7 +301,7 @@ class TestListener
                        var player = new Player(game, "Morriar")
                        player.stats["comments"] = i
                        player.save
-                       l.apply_event(generator.issue_comment_event(pull, comment), db)
+                       l.apply_event(generator.issue_comment_event(repo, pull, comment), db)
                        assert player.load_achievements.has_key(id)
                end
                var player = new Player(game, "Morriar")
@@ -318,7 +318,7 @@ class TestListener
                assert issue != null
 
                issue.title = "nitdoc ffi"
-               l.apply_event(generator.issue_open(issue), db)
+               l.apply_event(generator.issue_open(repo, issue), db)
                var player = new Player(game, "Morriar")
                assert player.load_achievements.has_key("issue_about_nitdoc")
                assert player.load_achievements.has_key("issue_about_ffi")
@@ -337,19 +337,19 @@ class TestListener
                assert comment != null
 
                comment.body = "@{game.repo.owner.login}"
-               l.apply_event(generator.issue_comment_event(pull, comment), db)
+               l.apply_event(generator.issue_comment_event(repo, pull, comment), db)
                var player = new Player(game, "Morriar")
                assert player.load_achievements.has_key("player_ping_god")
                assert player.stats.overall["nitcoins"] == 50
 
                comment.body = "+1"
-               l.apply_event(generator.issue_comment_event(pull, comment), db)
+               l.apply_event(generator.issue_comment_event(repo, pull, comment), db)
                player = new Player(game, "Morriar")
                assert player.load_achievements.has_key("player_first_review")
                assert player.stats.overall["nitcoins"] == 60
 
                comment.body = "Nitcoins"
-               l.apply_event(generator.issue_comment_event(pull, comment), db)
+               l.apply_event(generator.issue_comment_event(repo, pull, comment), db)
                player = new Player(game, "Morriar")
                assert player.load_achievements.has_key("player_says_nitcoin")
                assert player.stats.overall["nitcoins"] == 70
index 850bb6f..a1dcad4 100644 (file)
@@ -20,6 +20,7 @@
 module api
 
 import github_curl
+intrude import json::serialization
 
 # Client to Github API
 #
@@ -88,19 +89,27 @@ class GithubAPI
                ghcurl = new GithubCurl(auth, user_agent)
        end
 
+       # Deserialize an object
+       fun deserialize(string: String): nullable Object do
+               var deserializer = new GithubDeserializer(string)
+               var res = deserializer.deserialize
+               # print deserializer.errors.join("\n") # DEBUG
+               return res
+       end
+
        # Execute a GET request on Github API.
        #
        # This method returns raw json data.
        # See other `load_*` methods to use more expressive types.
        #
        #     var api = new GithubAPI(get_github_oauth)
-       #     var obj = api.get("repos/nitlang/nit")
+       #     var obj = api.get("/repos/nitlang/nit")
        #     assert obj isa JsonObject
        #     assert obj["name"] == "nit"
        #
        # Returns `null` in case of `error`.
        #
-       #     obj = api.get("foo/bar/baz")
+       #     obj = api.get("/foo/bar/baz")
        #     assert obj == null
        #     assert api.was_error
        #     var err = api.last_error
@@ -109,7 +118,7 @@ class GithubAPI
        #     assert err.message == "Not Found"
        fun get(path: String): nullable Jsonable do
                path = sanitize_uri(path)
-               var res = ghcurl.get_and_parse("{api_url}/{path}")
+               var res = ghcurl.get_and_parse("{api_url}{path}")
                if res isa Error then
                        last_error = res
                        was_error = true
@@ -138,11 +147,11 @@ class GithubAPI
 
        # Load the json object from Github.
        # See `GithubEntity::load_from_github`.
-       protected fun load_from_github(key: String): JsonObject do
+       protected fun load_from_github(key: String): nullable GithubEntity do
                message(1, "Get {key} (github)")
                var res = get(key)
-               if was_error then return new JsonObject
-               return res.as(JsonObject)
+               if was_error then return null
+               return deserialize(res.as(JsonObject).to_json).as(nullable GithubEntity)
        end
 
        # Get the Github logged user from `auth` token.
@@ -155,9 +164,9 @@ class GithubAPI
        # assert user.login == "Morriar"
        # ~~~
        fun load_auth_user: nullable User do
-               var json = load_from_github("user")
+               var user = load_from_github("/user")
                if was_error then return null
-               return new User.from_json(self, json)
+               return user.as(nullable User)
        end
 
        # Get the Github user with `login`
@@ -166,10 +175,10 @@ class GithubAPI
        #
        #     var api = new GithubAPI(get_github_oauth)
        #     var user = api.load_user("Morriar")
+       #     print user or else "null"
        #     assert user.login == "Morriar"
        fun load_user(login: String): nullable User do
-               var user = new User(self, login)
-               return user.load_from_github
+               return load_from_github("/users/{login}").as(nullable User)
        end
 
        # Get the Github repo with `full_name`.
@@ -180,10 +189,110 @@ class GithubAPI
        #     var repo = api.load_repo("nitlang/nit")
        #     assert repo.name == "nit"
        #     assert repo.owner.login == "nitlang"
-       #     assert repo.default_branch.name == "master"
+       #     assert repo.default_branch == "master"
        fun load_repo(full_name: String): nullable Repo do
-               var repo = new Repo(self, full_name)
-               return repo.load_from_github
+               return load_from_github("/repos/{full_name}").as(nullable Repo)
+       end
+
+       # List of branches associated with their names.
+       fun load_repo_branches(repo: Repo): Array[Branch] do
+               message(1, "Get branches for {repo.full_name}")
+               var array = get("/repos/{repo.full_name}/branches")
+               var res = new Array[Branch]
+               if not array isa JsonArray then return res
+               return deserialize(array.to_json).as(Array[Branch])
+       end
+
+       # List of issues associated with their ids.
+       fun load_repo_issues(repo: Repo): Array[Issue] do
+               message(1, "Get issues for {repo.full_name}")
+               var res = new Array[Issue]
+               var issue = load_repo_last_issue(repo)
+               if issue == null then return res
+               res.add issue
+               while issue != null and issue.number > 1 do
+                       issue = load_issue(repo, issue.number - 1)
+                       if issue == null then continue
+                       res.add 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_repo_issues(repo: Repo, query: String): Array[Issue] do
+               query = "/search/issues?q={query} repo:{repo.full_name}"
+               var res = new Array[Issue]
+               var response = get(query)
+               if was_error then return res
+               var arr = response.as(JsonObject)["items"].as(JsonArray)
+               return deserialize(arr.to_json).as(Array[Issue])
+       end
+
+       # Get the last published issue.
+       fun load_repo_last_issue(repo: Repo): nullable Issue do
+               var array = get("/repos/{repo.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 deserialize(obj.to_json).as(nullable Issue)
+       end
+
+       # List of labels associated with their names.
+       fun load_repo_labels(repo: Repo): Array[Label] do
+               message(1, "Get labels for {repo.full_name}")
+               var array = get("repos/{repo.full_name}/labels")
+               if not array isa JsonArray then return new Array[Label]
+               return deserialize(array.to_json).as(Array[Label])
+       end
+
+       # List of milestones associated with their ids.
+       fun load_repo_milestones(repo: Repo): Array[Milestone] do
+               message(1, "Get milestones for {repo.full_name}")
+               var array = get("/repos/{repo.full_name}/milestones")
+               if not array isa JsonArray then return new Array[Milestone]
+               return deserialize(array.to_json).as(Array[Milestone])
+       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 load_repo_pulls(repo: Repo): Array[PullRequest] do
+               message(1, "Get pulls for {repo.full_name}")
+               var key = "/repos/{repo.full_name}"
+               var res = new Array[PullRequest]
+               var page = 1
+               loop
+                       var array = get("{key}/pulls?page={page}").as(JsonArray)
+                       if array.is_empty then break
+                       for obj in array do
+                               if not obj isa JsonObject then continue
+                               var pr = deserialize(array.to_json).as(nullable PullRequest)
+                               if pr == null then continue
+                               res.add pr
+                       end
+                       page += 1
+               end
+               return res
+       end
+
+       # List of contributor related statistics.
+       fun load_repo_contrib_stats(repo: Repo): Array[ContributorStats] do
+               message(1, "Get contributor stats for {repo.full_name}")
+               var res = new Array[ContributorStats]
+               var array = get("/repos/{repo.full_name}/stats/contributors")
+               if not array isa JsonArray then return res
+               return deserialize(array.to_json).as(Array[ContributorStats])
        end
 
        # Get the Github branch with `name`.
@@ -197,8 +306,31 @@ class GithubAPI
        #     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)
-               return branch.load_from_github
+               return load_from_github("/repos/{repo.full_name}/branches/{name}").as(nullable Branch)
+       end
+
+       # List all commits in `self`.
+       #
+       # This can be long depending on the branch size.
+       # Commit are returned in an unspecified order.
+       fun load_branch_commits(branch: Branch): Array[Commit] do
+               var res = new Array[Commit]
+               var done = new HashSet[String]
+               var todos = new Array[Commit]
+               todos.add branch.commit
+               loop
+                       if todos.is_empty then break
+                       var commit = todos.pop
+                       if done.has(commit.sha) then continue
+                       done.add commit.sha
+                       res.add commit
+                       var parents = commit.parents
+                       if parents == null then continue
+                       for parent in parents do
+                               todos.add parent
+                       end
+               end
+               return res
        end
 
        # Get the Github commit with `sha`.
@@ -211,8 +343,7 @@ class GithubAPI
        #     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)
-               return commit.load_from_github
+               return load_from_github("/repos/{repo.full_name}/commits/{sha}").as(nullable Commit)
        end
 
        # Get the Github issue #`number`.
@@ -225,8 +356,47 @@ class GithubAPI
        #     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
+               return load_from_github("/repos/{repo.full_name}/issues/{number}").as(nullable Issue)
+       end
+
+       # List of event on this issue.
+       fun load_issue_comments(repo: Repo, issue: Issue): Array[IssueComment] do
+               var res = new Array[IssueComment]
+               var count = issue.comments or else 0
+               var page = 1
+               loop
+                       var array = get("/repos/{repo.full_name}/comments?page={page}")
+                       if not array isa JsonArray then break
+                       if array.is_empty or res.length < count then break
+                       for obj in array do
+                               if not obj isa JsonObject then continue
+                               var id = obj["id"].as(Int)
+                               var comment = load_issue_comment(repo, id)
+                               if comment == null then continue
+                               res.add(comment)
+                       end
+                       page += 1
+               end
+               return res
+       end
+
+       # List of events on this issue.
+       fun load_issue_events(repo: Repo, issue: Issue): Array[IssueEvent] do
+               var res = new Array[IssueEvent]
+               var key = "/repos/{repo.full_name}/issues/{issue.number}"
+               var page = 1
+               loop
+                       var array = get("{key}/events?page={page}")
+                       if not array isa JsonArray or array.is_empty then break
+                       for obj in array do
+                               if not obj isa JsonObject then continue
+                               var event = deserialize(obj.to_json).as(nullable IssueEvent)
+                               if event == null then continue
+                               res.add event
+                       end
+                       page += 1
+               end
+               return res
        end
 
        # Get the Github pull request #`number`.
@@ -240,8 +410,7 @@ class GithubAPI
        #     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
+               return load_from_github("/repos/{repo.full_name}/pulls/{number}").as(nullable PullRequest)
        end
 
        # Get the Github label with `name`.
@@ -254,8 +423,7 @@ class GithubAPI
        #     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)
-               return labl.load_from_github
+               return load_from_github("/repos/{repo.full_name}/labels/{name}").as(nullable Label)
        end
 
        # Get the Github milestone with `id`.
@@ -268,8 +436,7 @@ class GithubAPI
        #     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)
-               return milestone.load_from_github
+               return load_from_github("/repos/{repo.full_name}/milestones/{id}").as(nullable Milestone)
        end
 
        # Get the Github issue event with `id`.
@@ -280,13 +447,13 @@ class GithubAPI
        #     var repo = api.load_repo("nitlang/nit")
        #     assert repo isa Repo
        #     var event = api.load_issue_event(repo, 199674194)
+       #     assert event isa IssueEvent
        #     assert event.actor.login == "privat"
        #     assert event.event == "labeled"
+       #     assert event.labl isa Label
        #     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
+               return load_from_github("/repos/{repo.full_name}/issues/events/{id}").as(nullable IssueEvent)
        end
 
        # Get the Github commit comment with `id`.
@@ -299,10 +466,9 @@ class GithubAPI
        #     var comment = api.load_commit_comment(repo, 8982707)
        #     assert comment.user.login == "Morriar"
        #     assert comment.body == "For testing purposes..."
-       #     assert comment.commit.sha == "7eacb86d1e24b7e72bc9ac869bf7182c0300ceca"
+       #     assert comment.commit_id == "7eacb86d1e24b7e72bc9ac869bf7182c0300ceca"
        fun load_commit_comment(repo: Repo, id: Int): nullable CommitComment do
-               var comment = new CommitComment(self, repo, id)
-               return comment.load_from_github
+               return load_from_github("/repos/{repo.full_name}/comments/{id}").as(nullable CommitComment)
        end
 
        # Get the Github issue comment with `id`.
@@ -315,10 +481,9 @@ class GithubAPI
        #     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
+       #     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
+               return load_from_github("/repos/{repo.full_name}/issues/comments/{id}").as(nullable IssueComment)
        end
 
        # Get the Github diff comment with `id`.
@@ -331,10 +496,9 @@ class GithubAPI
        #     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
+       #     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
+               return load_from_github("/repos/{repo.full_name}/pulls/comments/{id}").as(nullable ReviewComment)
        end
 end
 
@@ -342,34 +506,13 @@ end
 #
 # Mainly a Nit wrapper around a JSON objet.
 abstract class GithubEntity
-
-       # Github API instance.
-       var api: GithubAPI
-
-       # FIXME constructor should be private
-
-       # Key used to access this entity from Github api base.
-       fun key: String is abstract
-
-       # JSON representation of `self`.
-       #
-       # This is the same json structure than used by Github API.
-       var json: JsonObject is noinit, protected writable
-
-       # Load `json` from Github API.
-       private fun load_from_github: nullable SELF do
-               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
+       super Jsonable
+       serialize
 
        # Github page url.
-       fun html_url: String do return json["html_url"].as(String)
+       var html_url: nullable String is writable
 
-       # Set page url.
-       fun html_url=(url: String) do json["html_url"] = url
+       redef fun to_json do return serialize_to_json
 end
 
 # A Github user
@@ -378,23 +521,13 @@ end
 # Should be accessed from `GithubAPI::load_user`.
 class User
        super GithubEntity
-
-       redef var key is lazy do return "users/{login}"
+       serialize
 
        # Github login.
-       var login: String
-
-       # Init `self` from a `json` object.
-       init from_json(api: GithubAPI, json: JsonObject) do
-               init(api, json["login"].as(String))
-               self.json = json
-       end
+       var login: String is writable
 
        # Avatar image url for this user.
-       fun avatar_url: String do return json["avatar_url"].as(String)
-
-       # Set avatar url.
-       fun avatar_url=(url: String) do json["avatar_url"] = url
+       var avatar_url: nullable String is writable
 end
 
 # A Github repository.
@@ -403,178 +536,19 @@ end
 # Should be accessed from `GithubAPI::load_repo`.
 class Repo
        super GithubEntity
-
-       redef var key is lazy do return "repos/{full_name}"
+       serialize
 
        # Repo full name on Github.
-       var full_name: String
-
-       # Init `self` from a `json` object.
-       init from_json(api: GithubAPI, json: JsonObject) do
-               init(api, json["full_name"].as(String))
-               self.json = json
-       end
+       var full_name: String is writable
 
        # Repo short name on Github.
-       fun name: String do return json["name"].as(String)
-
-       # Set repo full name
-       fun name=(name: String) do json["name"] = name
+       var name: String is writable
 
        # Get the repo owner.
-       fun owner: User do return new User.from_json(api, json["owner"].as(JsonObject))
-
-       # Set repo owner
-       fun owner=(owner: User) do json["owner"] = owner.json
-
-       # List of branches associated with their names.
-       fun branches: Map[String, Branch] do
-               api.message(1, "Get branches for {full_name}")
-               var array = api.get("repos/{full_name}/branches")
-               var res = new HashMap[String, Branch]
-               if not array isa JsonArray then return res
-               for obj in array do
-                       if not obj isa JsonObject then continue
-                       var name = obj["name"].as(String)
-                       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}")
-               var array = api.get("repos/{full_name}/labels")
-               var res = new HashMap[String, Label]
-               if not array isa JsonArray then return res
-               for obj in array do
-                       if not obj isa JsonObject then continue
-                       var name = obj["name"].as(String)
-                       res[name] = new Label.from_json(api, self, obj)
-               end
-               return res
-       end
-
-       # List of milestones associated with their ids.
-       fun milestones: Map[Int, Milestone] do
-               api.message(1, "Get milestones for {full_name}")
-               var array = api.get("repos/{full_name}/milestones")
-               var res = new HashMap[Int, Milestone]
-               if array isa JsonArray then
-                       for obj in array do
-                               if not obj isa JsonObject then continue
-                               var number = obj["number"].as(Int)
-                               res[number] = new Milestone.from_json(api, self, obj)
-                       end
-               end
-               return res
-       end
+       var owner: User is writable
 
-       # 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
-               var name = json["default_branch"].as(String)
-               var branch = api.load_branch(self, name)
-               assert branch isa Branch
-               return branch
-       end
-
-       # Set the default branch
-       fun default_branch=(branch: Branch) do json["default_branch"] = branch.json
-end
-
-# A `RepoEntity` is something contained in a `Repo`.
-abstract class RepoEntity
-       super GithubEntity
-
-       # Repo that contains `self`.
-       var repo: Repo
-
-       # Init `self` from a `json` object.
-       init from_json(api: GithubAPI, repo: Repo, json: JsonObject) do
-               init(api, repo)
-               self.json = json
-       end
+       # Repo default branch name.
+       var default_branch: String is writable
 end
 
 # A Github branch.
@@ -583,44 +557,14 @@ end
 #
 # See <https://developer.github.com/v3/repos/#list-branches>.
 class Branch
-       super RepoEntity
-
-       redef var key is lazy do return "{repo.key}/branches/{name}"
+       super GithubEntity
+       serialize
 
        # Branch name.
-       var name: String
-
-       redef init from_json(api, repo, json) do
-               self.name = json["name"].as(String)
-               super
-       end
+       var name: String is writable
 
        # Get the last commit of `self`.
-       fun commit: Commit do return new Commit.from_json(api, repo, json["commit"].as(JsonObject))
-
-       # Set the last commit
-       fun commit=(commit: Commit) do json["commit"] = commit.json
-
-       # List all commits in `self`.
-       #
-       # This can be long depending on the branch size.
-       # Commit are returned in an unspecified order.
-       fun commits: Array[Commit] do
-               var res = new Array[Commit]
-               var done = new HashSet[String]
-               var todos = new Array[Commit]
-               todos.add commit
-               while not todos.is_empty do
-                       var commit = todos.pop
-                       if done.has(commit.sha) then continue
-                       done.add commit.sha
-                       res.add commit
-                       for parent in commit.parents do
-                               todos.add parent
-                       end
-               end
-               return res
-       end
+       var commit: Commit is writable
 end
 
 # A Github commit.
@@ -629,103 +573,46 @@ end
 #
 # See <https://developer.github.com/v3/repos/commits/>.
 class Commit
-       super RepoEntity
-
-       redef var key is lazy do return "{repo.key}/commits/{sha}"
+       super GithubEntity
+       serialize
 
        # Commit SHA.
-       var sha: String
-
-       redef init from_json(api, repo, json) do
-               self.sha = json["sha"].as(String)
-               super
-       end
+       var sha: String is writable
 
        # Parent commits of `self`.
-       fun parents: Array[Commit] do
-               var res = new Array[Commit]
-               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
-                       res.add(api.load_commit(repo, obj["sha"].as(String)).as(not null))
-               end
-               return res
-       end
-
-       # Set parent commits.
-       fun parents=(parents: Array[Commit]) do
-               var res = new JsonArray
-               for parent in parents do res.add parent.json
-               json["parents"] = res
-       end
+       var parents: nullable Array[Commit] = null is writable
 
        # Author of the commit.
-       fun author: nullable User do
-               var user = json.get_or_null("author")
-               if user isa JsonObject then return new User.from_json(api, user)
-               return null
-       end
-
-       # Set commit author.
-       fun author=(user: nullable User) do
-               if user == null then
-                       json["author"] = null
-               else
-                       json["author"] = user.json
-               end
-       end
+       var author: nullable User is writable
 
        # Committer of the commit.
-       fun committer: nullable User do
-               var user = json.get_or_null("author")
-               if user isa JsonObject then return new User.from_json(api, user)
-               return null
-       end
+       var committer: nullable User is writable
 
-       # Set commit committer.
-       fun committer=(user: nullable User) do
-               if user == null then
-                       json["committer"] = null
-               else
-                       json["committer"] = user.json
-               end
-       end
+       # Authoring date as String.
+       var author_date: nullable String is writable
 
        # 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"].as(String))
+       fun iso_author_date: nullable ISODate do
+               var author_date = self.author_date
+               if author_date == null then return null
+               return new ISODate.from_string(author_date)
        end
 
+       # Commit date as String.
+       var commit_date: nullable String is writable
+
        # 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"].as(String))
+       fun iso_commit_date: nullable ISODate do
+               var commit_date = self.commit_date
+               if commit_date == null then return null
+               return new ISODate.from_string(commit_date)
        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
-
-       # Set commit files.
-       fun files=(files: Array[GithubFile]) do
-               var res = new JsonArray
-               for file in files do res.add file.json
-               json["files"] = res
-       end
+       var files: nullable Array[GithubFile] = null is optional, writable
 
        # Commit message.
-       fun message: String do return json["commit"].as(JsonObject)["message"].as(String)
+       var message: nullable String is writable
 end
 
 # A Github issue.
@@ -734,210 +621,75 @@ end
 #
 # See <https://developer.github.com/v3/issues/>.
 class Issue
-       super RepoEntity
-
-       redef var key is lazy do return "{repo.key}/issues/{number}"
+       super GithubEntity
+       serialize
 
        # Issue Github ID.
-       var number: Int
-
-       redef init from_json(api, repo, json) do
-               self.number = json["number"].as(Int)
-               super
-       end
+       var number: Int is writable
 
        # Issue id.
-       fun id: Int do return json["id"].as(Int)
-
-       # Set issue id.
-       fun id=(id: Int) do json["id"] = id
+       var id: nullable Int is writable
 
        # Issue title.
-       fun title: String do return json["title"].as(String)
-
-       # Set issue title
-       fun title=(title: String) do json["title"] = title
+       var title: String is writable
 
        # User that created this issue.
-       fun user: User do return new User.from_json(api, json["user"].as(JsonObject))
-
-       # Set issue creator.
-       fun user=(user: User) do json["user"] = user.json
+       var user: nullable User is writable
 
        # 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
+       var labels: nullable Array[Label] is writable
 
        # State of the issue on Github.
-       fun state: String do return json["state"].as(String)
-
-       # Set the state of this issue.
-       fun state=(state: String) do json["state"] = state
+       var state: String is writable
 
        # Is the issue locked?
-       fun locked: Bool do return json["locked"].as(Bool)
-
-       # Set issue locked state.
-       fun locked=(locked: Bool) do json["locked"] = locked
+       var locked: nullable Bool is writable
 
        # 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
-
-       # Set issue assignee.
-       fun assignee=(user: nullable User) do
-               if user == null then
-                       json["assignee"] = null
-               else
-                       json["assignee"] = user.json
-               end
-       end
+       var assignee: nullable User is writable
 
        # `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
-
-       # Set issue milestone.
-       fun milestone=(milestone: nullable Milestone) do
-               if milestone == null then
-                       json["milestone"] = null
-               else
-                       json["milestone"] = milestone.json
-               end
-       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)
-                               var comment = api.load_issue_comment(repo, id)
-                               if comment == null then continue
-                               res.add(comment)
-                       end
-                       page += 1
-                       var json = api.get("{key}/comments?page={page}")
-                       if not json isa JsonArray then
-                               return res
-                       end
-                       array = json
-               end
-               return res
-       end
+       var milestone: nullable Milestone is writable
 
        # Number of comments on this issue.
-       fun comments_count: Int do return json["comments"].as(Int)
+       var comments: nullable Int is writable
 
-       # Creation time in ISODate format.
-       fun created_at: ISODate do return new ISODate.from_string(json["created_at"].as(String))
+       # Creation time as String.
+       var created_at: String is writable
 
-       # Set issue creation time.
-       fun created_at=(created_at: nullable ISODate) do
-               if created_at == null then
-                       json["created_at"] = null
-               else
-                       json["created_at"] = created_at.to_s
-               end
+       # Creation time as ISODate.
+       fun iso_created_at: ISODate do
+               return new ISODate.from_string(created_at)
        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
+       # Last update time as String (if any).
+       var updated_at: nullable String is writable
 
-       # Set issue last update time.
-       fun updated_at=(updated_at: nullable ISODate) do
-               if updated_at == null then
-                       json["updated_at"] = null
-               else
-                       json["updated_at"] = updated_at.to_s
-               end
+       # Last update date as ISODate.
+       fun iso_updated_at: nullable ISODate do
+               var updated_at = self.updated_at
+               if updated_at == null then return null
+               return new ISODate.from_string(updated_at)
        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
+       # Close time as String (if any).
+       var closed_at: nullable String is writable
 
-       # Set issue close time.
-       fun closed_at=(closed_at: nullable ISODate) do
-               if closed_at == null then
-                       json["closed_at"] = null
-               else
-                       json["closed_at"] = closed_at.to_s
-               end
+       # Close time as ISODate.
+       fun iso_closed_at: nullable ISODate do
+               var closed_at = self.closed_at
+               if closed_at == null then return null
+               return new ISODate.from_string(closed_at)
        end
 
-       # TODO link to pull request
-
        # Full description of the issue.
-       fun body: String do return json["body"].as(String)
-
-       # Set description body
-       fun body=(body: String) do json["body"] = body
-
-       # 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
+       var body: nullable String is writable
 
        # 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
-
-       # Set user that closed the issue.
-       fun closed_by=(user: nullable User) do
-               if user == null then
-                       json["closed_by"] = null
-               else
-                       json["closed_by"] = user.json
-               end
-       end
+       var closed_by: nullable User is writable
 
        # Is this issue linked to a pull request?
-       fun is_pull_request: Bool do return json.has_key("pull_request")
+       var is_pull_request: Bool is noserialize, writable
 end
 
 # A Github pull request.
@@ -948,158 +700,75 @@ end
 # See <https://developer.github.com/v3/pulls/>.
 class PullRequest
        super Issue
+       serialize
 
-       redef var key is lazy do return "{repo.key}/pulls/{number}"
+       # Merge time as String (if any).
+       var merged_at: nullable String is writable
 
-       # 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
-
-       # Set pull request merge time.
-       fun merged_at=(merged_at: nullable ISODate) do
-               if merged_at == null then
-                       json["merged_at"] = null
-               else
-                       json["merged_at"] = merged_at.to_s
-               end
+       # Merge time as ISODate.
+       fun iso_merged_at: nullable ISODate do
+               var merged_at = self.merged_at
+               if merged_at == null then return null
+               return new ISODate.from_string(merged_at)
        end
 
        # Merge commit SHA.
-       fun merge_commit_sha: String do return json["merge_commit_sha"].as(String)
-
-       # Set merge_commit_sha
-       fun merge_commit_sha=(sha: String) do json["merge_commit_sha"] = sha
+       var merge_commit_sha: nullable String is writable
 
        # Count of comments made on the pull request diff.
-       fun review_comments: Int do return json["review_comments"].as(Int)
-
-       # Set review_comments
-       fun review_comments=(count: Int) do json["review_comments"] = count
+       var review_comments: Int is writable
 
        # 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
-
-       # Set head
-       fun head=(head: PullRef) do json["head"] = head.json
+       var head: PullRef is writable
 
        # 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
-
-       # Set base
-       fun base=(base: PullRef) do json["base"] = base.json
+       var base: PullRef is writable
 
        # Is this pull request merged?
-       fun merged: Bool do return json["merged"].as(Bool)
-
-       # Set merged
-       fun merged=(merged: Bool) do json["merged"] = merged
+       var merged: Bool is writable
 
        # Is this pull request mergeable?
-       fun mergeable: Bool do return json["mergeable"].as(Bool)
-
-       # Set mergeable
-       fun mergeable=(mergeable: Bool) do json["mergeable"] = mergeable
+       var mergeable: nullable Bool is writable
 
        # 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)
-
-       # Set mergeable_state
-       fun mergeable_state=(mergeable_state: Int) do json["mergeable_state"] = mergeable_state
+       var mergeable_state: String is writable
 
        # 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
-
-       # Set merged_by.
-       fun merged_by=(merged_by: nullable User) do
-               if merged_by == null then
-                       json["merged_by"] = null
-               else
-                       json["merged_by"] = merged_by.json
-               end
-       end
+       var merged_by: nullable User is writable
 
        # Count of commits in this pull request.
-       fun commits: Int do return json["commits"].as(Int)
-
-       # Set commits
-       fun commits=(commits: Int) do json["commits"] = commits
+       var commits: Int is writable
 
        # Added line count.
-       fun additions: Int do return json["additions"].as(Int)
-
-       # Set additions
-       fun additions=(additions: Int) do json["additions"] = additions
+       var additions: Int is writable
 
        # Deleted line count.
-       fun deletions: Int do return json["deletions"].as(Int)
-
-       # Set deletions
-       fun deletions=(deletions: Int) do json["deletions"] = deletions
+       var deletions: Int is writable
 
        # Changed files count.
-       fun changed_files: Int do return json["changed_files"].as(Int)
-
-       # Set changed_files
-       fun changed_files=(changed_files: Int) do json["changed_files"] = changed_files
+       var changed_files: Int is writable
 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
+       serialize
 
        # Label pointed by `self`.
-       fun labl: String do return json["label"].as(String)
-
-       # Set labl
-       fun labl=(labl: String) do json["label"] = labl
+       var labl: String is writable, serialize_as("label")
 
        # Reference pointed by `self`.
-       fun ref: String do return json["ref"].as(String)
-
-       # Set ref
-       fun ref=(ref: String) do json["ref"] = ref
+       var ref: String is writable
 
        # Commit SHA pointed by `self`.
-       fun sha: String do return json["sha"].as(String)
-
-       # Set sha
-       fun sha=(sha: String) do json["sha"] = sha
+       var sha: String is writable
 
        # User pointed by `self`.
-       fun user: User do
-               return new User.from_json(api, json["user"].as(JsonObject))
-       end
-
-       # Set user
-       fun user=(user: User) do json["user"] = user.json
+       var user: User is writable
 
        # Repo pointed by `self`.
-       fun repo: Repo do
-               return new Repo.from_json(api, json["repo"].as(JsonObject))
-       end
-
-       # Set repo
-       fun repo=(repo: Repo) do json["repo"] = repo.json
+       var repo: Repo is writable
 end
 
 # A Github label.
@@ -1108,23 +777,14 @@ end
 #
 # See <https://developer.github.com/v3/issues/labels/>.
 class Label
-       super RepoEntity
-
-       redef var key is lazy do return "{repo.key}/labels/{name}"
+       super GithubEntity
+       serialize
 
        # Label name.
-       var name: String
-
-       redef init from_json(api, repo, json) do
-               self.name = json["name"].as(String)
-               super
-       end
+       var name: String is writable
 
        # Label color code.
-       fun color: String do return json["color"].as(String)
-
-       # Set color
-       fun color=(color: String) do json["color"] = color
+       var color: String is writable
 end
 
 # A Github milestone.
@@ -1133,110 +793,64 @@ end
 #
 # See <https://developer.github.com/v3/issues/milestones/>.
 class Milestone
-       super RepoEntity
-
-       redef var key is lazy do return "{repo.key}/milestones/{number}"
+       super GithubEntity
+       serialize
 
        # The milestone id on Github.
-       var number: Int
-
-       redef init from_json(api, repo, json) do
-               super
-               self.number = json["number"].as(Int)
-       end
+       var number: Int is writable
 
        # Milestone title.
-       fun title: String do return json["title"].as(String)
-
-       # Set title
-       fun title=(title: String) do json["title"] = title
+       var title: String is writable
 
        # Milestone long description.
-       fun description: String do return json["description"].as(String)
-
-       # Set description
-       fun description=(description: String) do json["description"] = description
+       var description: String is writable
 
        # Count of opened issues linked to this milestone.
-       fun open_issues: Int do return json["open_issues"].as(Int)
-
-       # Set open_issues
-       fun open_issues=(open_issues: Int) do json["open_issues"] = open_issues
+       var open_issues: Int is writable
 
        # Count of closed issues linked to this milestone.
-       fun closed_issues: Int do return json["closed_issues"].as(Int)
-
-       # Set closed_issues
-       fun closed_issues=(closed_issues: Int) do json["closed_issues"] = closed_issues
+       var closed_issues: Int is writable
 
        # Milestone state.
-       fun state: String do return json["state"].as(String)
-
-       # Set state
-       fun state=(state: String) do json["state"] = state
+       var state: String is writable
 
-       # Creation time in ISODate format.
-       fun created_at: ISODate do
-               return new ISODate.from_string(json["created_at"].as(String))
-       end
+       # Creation time as String.
+       var created_at: String is writable
 
-       # Set created_at
-       fun created_at=(created_at: ISODate) do json["created_at"] = created_at.to_s
+       # Creation time as ISODate.
+       fun iso_created_at: nullable ISODate do return new ISODate.from_string(created_at)
 
        # User that created this milestone.
-       fun creator: User do
-               return new User.from_json(api, json["creator"].as(JsonObject))
-       end
+       var creator: User is writable
 
-       # Set creator
-       fun creator=(creator: User) do json["creator"] = creator.json
+       # Due time as String (if any).
+       var due_on: nullable String is writable
 
        # Due time in ISODate format (if any).
-       fun due_on: 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
-
-       # Set due_on.
-       fun due_on=(due_on: nullable ISODate) do
-               if due_on == null then
-                       json["due_on"] = null
-               else
-                       json["due_on"] = due_on.to_s
-               end
+       fun iso_due_on: nullable ISODate do
+               var due_on = self.due_on
+               if due_on == null then return null
+               return new ISODate.from_string(due_on)
        end
 
-       # 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
+       # Last update time as String (if any).
+       var updated_at: nullable String is writable
 
-       # Set updated_at.
-       fun updated_at=(updated_at: nullable ISODate) do
-               if updated_at == null then
-                       json["updated_at"] = null
-               else
-                       json["updated_at"] = updated_at.to_s
-               end
+       # Last update date as ISODate.
+       fun iso_updated_at: nullable ISODate do
+               var updated_at = self.updated_at
+               if updated_at == null then return null
+               return new ISODate.from_string(updated_at)
        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
+       # Close time as String (if any).
+       var closed_at: nullable String is writable
 
-       # Set closed_at.
-       fun closed_at=(closed_at: nullable ISODate) do
-               if closed_at == null then
-                       json["closed_at"] = null
-               else
-                       json["closed_at"] = closed_at.to_s
-               end
+       # Close time as ISODate.
+       fun iso_closed_at: nullable ISODate do
+               var closed_at = self.closed_at
+               if closed_at == null then return null
+               return new ISODate.from_string(closed_at)
        end
 end
 
@@ -1248,57 +862,38 @@ end
 # * `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
+       super GithubEntity
+       serialize
 
        # Identifier of this comment.
-       var id: Int
-
-       redef init from_json(api, repo, json) do
-               self.id = json["id"].as(Int)
-               super
-       end
+       var id: Int is writable
 
        # User that made this comment.
-       fun user: User do
-               return new User.from_json(api, json["user"].as(JsonObject))
-       end
+       var user: User is writable
 
-       # Set user
-       fun user=(user: User) do json["user"] = user.json
+       # Creation time as String.
+       var created_at: String is writable
 
-       # Creation time in ISODate format.
-       fun created_at: ISODate do
-               return new ISODate.from_string(json["created_at"].as(String))
+       # Creation time as ISODate.
+       fun iso_created_at: nullable ISODate do
+               return new ISODate.from_string(created_at)
        end
 
-       # Set created_at
-       fun created_at=(created_at: ISODate) do json["created_at"] = created_at.to_s
-
-       # 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
+       # Last update time as String (if any).
+       var updated_at: nullable String is writable
 
-       # Set updated_at.
-       fun updated_at=(updated_at: nullable ISODate) do
-               if updated_at == null then
-                       json["updated_at"] = null
-               else
-                       json["updated_at"] = updated_at.to_s
-               end
+       # Last update date as ISODate.
+       fun iso_updated_at: nullable ISODate do
+               var updated_at = self.updated_at
+               if updated_at == null then return null
+               return new ISODate.from_string(updated_at)
        end
 
        # Comment body text.
-       fun body: String do return json["body"].as(String)
-
-       # Set body
-       fun body=(body: String) do json["body"] = body
+       var body: String is writable
 
        # Does the comment contain an acknowledgement (+1)
-       fun is_ack: Bool
-       do
+       fun is_ack: Bool do
                return body.has("\\+1\\b".to_re) or body.has(":+1:") or body.has(":shipit:")
        end
 end
@@ -1306,42 +901,19 @@ end
 # A comment made on a commit.
 class CommitComment
        super Comment
-
-       redef var key is lazy do return "{repo.key}/comments/{id}"
+       serialize
 
        # Commented commit.
-       fun commit: Commit do
-               return api.load_commit(repo, json["commit_id"].as(String)).as(not null)
-       end
-
-       # Set commit
-       fun commit=(commit: Commit) do json["commit_id"] = commit.json
+       var commit_id: String is writable
 
        # 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
-
-       # Set position.
-       fun position=(position: nullable String) do json["position"] = position
+       var position: nullable Int is writable
 
        # 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
-
-       # Set line.
-       fun line=(line: nullable String) do json["line"] = line
+       var line: nullable Int is writable
 
        # Path of the commented file.
-       fun path: String do return json["path"].as(String)
-
-       # Set path.
-       fun path=(path: String) do json["path"] = path
+       var path: nullable String is writable
 end
 
 # Comments made on Github issue and pull request pages.
@@ -1351,20 +923,13 @@ end
 # See <https://developer.github.com/v3/issues/comments/>.
 class IssueComment
        super Comment
-
-       redef var key is lazy do return "{repo.key}/issues/comments/{id}"
+       serialize
 
        # 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
+       fun issue_number: Int do return issue_url.split("/").last.to_i
 
        # Link to the issue document on API.
-       fun issue_url: String do return json["issue_url"].as(String)
-
-       # Set issue_url.
-       fun issue_url=(issue_url: String) do json["issue_url"] = issue_url
+       var issue_url: String is writable
 end
 
 # Comments made on Github pull request diffs.
@@ -1374,56 +939,31 @@ end
 # See <https://developer.github.com/v3/pulls/comments/>.
 class ReviewComment
        super Comment
-
-       redef var key is lazy do return "{repo.key}/pulls/comments/{id}"
+       serialize
 
        # 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
+       fun pull_number: Int do return pull_request_url.split("/").last.to_i
 
        # Link to the pull request on API.
-       fun pull_request_url: String do return json["pull_request_url"].as(String)
-
-       # Set pull_request_url.
-       fun pull_request_url=(pull_request_url: String) do json["pull_request_url"] = pull_request_url
+       var pull_request_url: String is writable
 
        # Diff hunk.
-       fun diff_hunk: String do return json["diff_hunk"].as(String)
-
-       # Set diff_hunk.
-       fun diff_hunk=(diff_hunk: String) do json["diff_hunk"] = diff_hunk
+       var diff_hunk: String is writable
 
        # Path of commented file.
-       fun path: String do return json["path"].as(String)
-
-       # Set path.
-       fun path=(path: String) do json["path"] = path
+       var path: String is writable
 
        # Position of the comment on the file.
-       fun position: Int do return json["position"].as(Int)
-
-       # Set position.
-       fun position=(position: Int) do json["position"] = position
+       var position: nullable Int is writable
 
        # Original position in the diff.
-       fun original_position: Int do return json["original_position"].as(Int)
-
-       # Set original_position.
-       fun original_position=(original_position: Int) do json["original_position"] = original_position
+       var original_position: Int is writable
 
        # Commit referenced by this comment.
-       fun commit_id: String do return json["commit_id"].as(String)
-
-       # Set commit_id.
-       fun commit_id=(commit_id: String) do json["commit_id"] = commit_id
+       var commit_id: String is writable
 
        # Original commit id.
-       fun original_commit_id: String do return json["original_commit_id"].as(String)
-
-       # Set original_commit_id.
-       fun original_commit_id=(commit_id: String) do json["original_commit_id"] = commit_id
+       var original_commit_id: String is writable
 end
 
 # An event that occurs on a Github `Issue`.
@@ -1432,182 +972,74 @@ end
 #
 # See <https://developer.github.com/v3/issues/events/>.
 class IssueEvent
-       super RepoEntity
-
-       redef var key is lazy do return "{repo.key}/issues/events/{id}"
+       super GithubEntity
+       serialize
 
        # 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
-
-       # Set issue.
-       fun issue=(issue: Issue) do json["issue"] = issue.json
+       var id: Int is writable
 
        # User that initiated the event.
-       fun actor: User do
-               return new User.from_json(api, json["actor"].as(JsonObject))
-       end
+       var actor: User is writable
 
-       # Set actor.
-       fun actor=(actor: User) do json["actor"] = actor.json
+       # Creation time as String.
+       var created_at: String is writable
 
-       # Creation time in ISODate format.
-       fun created_at: ISODate do
-               return new ISODate.from_string(json["created_at"].as(String))
+       # Creation time as ISODate.
+       fun iso_created_at: nullable ISODate do
+               return new ISODate.from_string(created_at)
        end
 
-       # Set created_at.
-       fun created_at=(created_at: ISODate) do json["created_at"] = created_at.to_s
-
        # Event descriptor.
-       fun event: String do return json["event"].as(String)
-
-       # Set event.
-       fun event=(event: String) do json["event"] = event
+       var event: String is writable
 
        # 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
-
-       # Set commit_id.
-       fun commit_id=(commit_id: nullable String) do json["commit_id"] = commit_id
+       var commit_id: nullable String is writable
 
        # 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
-
-       # Set labl.
-       fun labl=(labl: nullable Label) do
-               if labl == null then
-                       json["labl"] = null
-               else
-                       json["labl"] = labl.json
-               end
-       end
+       var labl: nullable Label is writable, serialize_as("label")
 
        # 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
-
-       # Set assignee.
-       fun assignee=(assignee: nullable User) do
-               if assignee == null then
-                       json["assignee"] = null
-               else
-                       json["assignee"] = assignee.json
-               end
-       end
+       var assignee: nullable User is writable
 
        # 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
-
-       # Set milestone.
-       fun milestone=(milestone: nullable User) do
-               if milestone == null then
-                       json["milestone"] = null
-               else
-                       json["milestone"] = milestone.json
-               end
-       end
+       var milestone: nullable Milestone is writable
 
        # 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
-
-       # Set rename.
-       fun rename=(rename: nullable User) do
-               if rename == null then
-                       json["rename"] = null
-               else
-                       json["rename"] = rename.json
-               end
-       end
+       var rename: nullable RenameAction is writable
 end
 
 # A rename action maintains the name before and after a renaming action.
 class RenameAction
-
-       # JSON content.
-       var json: JsonObject
+       serialize
 
        # Name before renaming.
-       fun from: String do return json["from"].as(String)
-
-       # Set from.
-       fun from=(from: String) do json["from"] = from
+       var from: String is writable
 
        # Name after renaming.
-       fun to: String do return json["to"].as(String)
-
-       # Set to.
-       fun to=(to: String) do json["to"] = to
+       var to: String is writable
 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
+       serialize
 
        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
-               init(api, json)
-       end
+       var api: GithubAPI is writable
 
        # User these statistics are about.
-       fun author: User do
-               return new User.from_json(api, json["author"].as(JsonObject))
-       end
-
-       # Set author.
-       fun author=(author: User) do json["author"] = author.json
+       var author: User is writable
 
        # Total number of commit.
-       fun total: Int do return json["total"].as(Int)
-
-       # Set total.
-       fun total=(total: Int) do json["total"] = total
+       var total: Int is writable
 
        # Are of weeks of activity with detailed statistics.
-       fun weeks: JsonArray do return json["weeks"].as(JsonArray)
-
-       # Set weeks.
-       fun weeks=(weeks: JsonArray) do json["weeks"] = weeks
+       var weeks: JsonArray is writable
 
        # ContributorStats can be compared on the total amount of commits.
        redef fun <(o) do return total < o.total
@@ -1617,13 +1049,52 @@ end
 #
 # Mostly a wrapper around a json object.
 class GithubFile
-
-       # Json content.
-       var json: JsonObject
+       serialize
 
        # File name.
-       fun filename: String do return json["filename"].as(String)
+       var filename: String is writable
+end
 
-       # Set filename.
-       fun filename=(filename: String) do json["filename"] = filename
+# Make ISO Datew serilizable
+redef class ISODate
+       super Jsonable
+       serialize
+
+       redef fun to_json do return serialize_to_json
+end
+
+# JsonDeserializer specific for Github objects.
+class GithubDeserializer
+       super JsonDeserializer
+
+       redef fun class_name_heuristic(json_object) do
+               if json_object.has_key("login") or json_object.has_key("email") then
+                       return "User"
+               else if json_object.has_key("full_name") then
+                       return "Repo"
+               else if json_object.has_key("name") and json_object.has_key("commit") then
+                       return "Branch"
+               else if json_object.has_key("sha") and json_object.has_key("ref") then
+                       return "PullRef"
+               else if json_object.has_key("sha") or (json_object.has_key("id") and json_object.has_key("tree_id")) then
+                       return "Commit"
+               else if json_object.has_key("number") and json_object.has_key("patch_url") then
+                       return "PullRequest"
+               else if json_object.has_key("open_issues") and json_object.has_key("closed_issues") then
+                       return "Milestone"
+               else if json_object.has_key("number") and json_object.has_key("title") then
+                       return "Issue"
+               else if json_object.has_key("color") then
+                       return "Label"
+               else if json_object.has_key("event") then
+                       return "IssueEvent"
+               else if json_object.has_key("original_commit_id") then
+                       return "ReviewComment"
+               else if json_object.has_key("commit_id") then
+                       return "CommitComment"
+               else if json_object.has_key("issue_url") then
+                       return "IssueComment"
+               end
+               return null
+       end
 end
index 61a06cc..466e4a9 100644 (file)
@@ -60,17 +60,19 @@ redef class GithubAPI
                if store.has_key(key) then
                        message(1, "Get {key} (cache)")
                        was_error = false
-                       return store.load_object(key)
+                       return deserialize(store.load_object(key).to_json).as(nullable GithubEntity)
                end
                var obj = super
-               if not was_error then cache(key, obj)
+               if not was_error then
+                       cache(key, obj.as(not null))
+               end
                return obj
        end
 
        # Save `json` data in cache under `key`.
-       private fun cache(key: String, json: JsonObject) do
+       private fun cache(key: String, obj: GithubEntity) do
                message(2, "Cache key {key}")
-               store.store_object(key, json)
+               store.store_object(key, obj.to_json.parse_json.as(JsonObject))
        end
 
        # Check if a cache file exists for `key`.
index 33c637f..2dc0af7 100644 (file)
 module events
 
 import api
+intrude import json::serialization
 
 # Github event stub.
 class GithubEvent
-
-       # Github API client.
-       var api: GithubAPI
-
-       # Json representation of `self`.
-       var json: JsonObject is noinit
-
-       init do
-               json = new JsonObject
-       end
-
-       # Init `self` from a `json` object.
-       init from_json(api: GithubAPI, json: JsonObject) do
-               init(api)
-               self.json = json
-       end
+       super Jsonable
+       serialize
 
        # Event ID from Github.
-       fun id: String do return json["id"].as(String)
-
-       # Set id.
-       fun id=(id: String) do json["id"] = id
+       var id: nullable String is writable
 
        # Action performed by the event.
-       fun action: String do return json["action"].as(String)
-
-       # Set action.
-       fun action=(action: String) do json["action"] = action
+       var action: nullable String is writable
 
        # Repo where this event occured.
-       fun repo: Repo do
-               return new Repo.from_json(api, json["repository"].as(JsonObject))
-       end
+       var repo: Repo is writable
 
-       # Set repo.
-       fun repo=(repo: Repo) do json["repository"] = repo.json
+       redef fun to_json do return serialize_to_json
 end
 
 # Triggered when a commit comment is created.
 class CommitCommentEvent
        super GithubEvent
+       serialize
 
        # The `Comment` itself.
-       fun comment: CommitComment do
-               return new CommitComment.from_json(api, repo, json["comment"].as(JsonObject))
-       end
-
-       # Set comment.
-       fun comment=(comment: CommitComment) do json["comment"] = comment.json
+       var comment: CommitComment is writable
 end
 
 # Triggered when a repository, branch, or tag is created.
 class CreateEvent
        super GithubEvent
+       serialize
 
        # Oject type that was created.
        #
        # Can be one of `repository`, `branch`, or `tag`.
-       fun ref_type: String do return json["ref_type"].as(String)
-
-       # Set ref_type.
-       fun ref_type=(ref_type: String) do json["ref_type"] = ref_type
+       var ref_type: String is writable
 
        # Git ref (or null if only a repository was created).
-       fun ref: String do return json["ref"].as(String)
-
-       # Set ref.
-       fun ref=(ref: String) do json["ref"] = ref
+       var ref: String is writable
 
        # Name of the repo's default branch (usually master).
-       fun master_branch: String do return json["master_branch"].as(String)
-
-       # Set master_branch.
-       fun master_branch=(master_branch: String) do json["master_branch"] = master_branch
+       var master_branch: String is writable
 
        # Repo's current description.
-       fun description: String do return json["description"].as(String)
-
-       # Set description.
-       fun description=(description: String) do json["description"] = description
+       var description: nullable String is writable
 end
 
 # Triggered when a branch or a tag is deleted.
 class DeleteEvent
        super GithubEvent
+       serialize
 
        # Object type that was deleted.
        #
        # Can be one of `repository`, `branch`, or `tag`.
-       fun ref_type: String do return json["ref_type"].as(String)
-
-       # Set ref_type.
-       fun ref_type=(ref_type: String) do json["ref_type"] = ref_type
+       var ref_type: String is writable
 
        # Git ref (or null if only a repository was deleted).
-       fun ref: String do return json["ref"].as(String)
-
-       # Set ref.
-       fun ref=(ref: String) do json["ref"] = ref
+       var ref: String is writable
 end
 
 # Triggered when a new snapshot is deployed.
@@ -127,112 +85,64 @@ end
 # Deployement are mainly used with integration testing servers.
 class DeploymentEvent
        super GithubEvent
+       serialize
 
        # Commit SHA for which this deployment was created.
-       fun sha: String do return json["sha"].as(String)
-
-       # Set sha.
-       fun sha=(sha: String) do json["sha"] = sha
+       var sha: String is writable
 
        # Name of repository for this deployment, formatted as :owner/:repo.
-       fun name: String do return json["name"].as(String)
-
-       # Set name.
-       fun name=(name: String) do json["name"] = name
+       var name: String is writable
 
        # Optional extra information for this deployment.
-       fun payload: nullable String do
-               var res = json.get_or_null("payload")
-               if res isa String then return res else return null
-       end
-
-       # Set payload.
-       fun payload=(payload: nullable String) do json["payload"] = payload
+       var payload: nullable String is writable
 
        # Optional environment to deploy to.
        # Default: "production"
-       fun environment: nullable String do
-               var res = json.get_or_null("environment")
-               if res isa String then return res else return null
-       end
-
-       # Set environment.
-       fun environment=(environment: nullable String) do json["environment"] = environment
+       var environment: nullable String is writable
 
        # Optional human-readable description added to the deployment.
-       fun description: nullable String do
-               var res = json.get_or_null("description")
-               if res isa String then return res else return null
-       end
-
-       # Set description.
-       fun description=(description: nullable String) do json["description"] = description
+       var description: nullable String is writable
 end
 
 # Triggered when a deployement's status changes.
 class DeploymentStatusEvent
        super GithubEvent
+       serialize
 
        # New deployment state.
        #
        # Can be `pending`, `success`, `failure`, or `error`.
-       fun state: String do return json["state"].as(String)
+       var state: String is writable
 
        # Optional link added to the status.
-       fun target_url: nullable String do
-               var res = json.get_or_null("target_url")
-               if res isa String then return res else return null
-       end
-
-       # Set target_url.
-       fun target_url=(target_url: nullable String) do json["target_url"] = target_url
+       var target_url: nullable String is writable
 
        # Deployment hash that this status is associated with.
-       fun deployment: String do return json["deployment"].as(String)
-
-       # Set deployment.
-       fun deployment=(deployment: String) do json["deployment"] = deployment
+       var deployment: String is writable
 
        # Optional human-readable description added to the status.
-       fun description: nullable String do
-               var res = json.get_or_null("description")
-               if res isa String then return res else return null
-       end
-
-       # Set description.
-       fun description=(description: nullable String) do json["description"] = description
+       var description: nullable String is writable
 end
 
 # Triggered when a user forks a repository.
 class ForkEvent
        super GithubEvent
+       serialize
 
        # Created repository.
-       fun forkee: Repo do return new Repo.from_json(api, json["forkee"].as(JsonObject))
-
-       # Set forkee.
-       fun forkee=(forkee: Repo) do json["forkee"] = forkee.json
+       var forkee: Repo is writable
 end
 
 # Triggered when an issue comment is created.
 class IssueCommentEvent
        super GithubEvent
+       serialize
 
        # `Issue` the comment belongs to.
-       fun issue: Issue do
-               return new Issue.from_json(api, repo, json["issue"].as(JsonObject))
-       end
-
-       # Set issue.
-       fun issue=(issue: Issue) do json["issue"] = issue.json
+       var issue: Issue is writable
 
        # The `Comment` itself.
-       fun comment: IssueComment do
-               return new IssueComment.from_json(api, repo, json["comment"].as(JsonObject))
-       end
-
-       # Set comment.
-       fun comment=(comment: IssueComment) do json["comment"] = comment.json
+       var comment: IssueComment is writable
 end
 
 # Triggered when an event occurs on an issue.
@@ -241,53 +151,25 @@ end
 # opened, closed or reopened.
 class IssuesEvent
        super GithubEvent
+       serialize
 
        # The `Issue` itself.
-       fun issue: Issue do return new Issue.from_json(api, repo, json["issue"].as(JsonObject))
-
-       # Set issue.
-       fun issue=(issue: Issue) do json["issue"] = issue.json
+       var issue: Issue is writable
 
        # Optional `Label` that was added or removed from the issue.
-       fun lbl: nullable Label do
-               var res = json.get_or_null("label")
-               if res isa JsonObject then return new Label.from_json(api, repo, res) else return null
-       end
-
-       # Set lbl.
-       fun lbl=(lbl: nullable Label) do
-               if lbl == null then
-                       json["lbl"] = null
-               else
-                       json["lbl"] = lbl.json
-               end
-       end
+       var lbl: nullable Label is writable, serialize_as("label")
 
        # Optional `User` that was assigned or unassigned from the issue.
-       fun assignee: nullable User do
-               var res = json.get_or_null("assignee")
-               if res isa JsonObject then return new User.from_json(api, res) else return null
-       end
-
-       # Set assignee.
-       fun assignee=(assignee: nullable User) do
-               if assignee == null then
-                       json["assignee"] = null
-               else
-                       json["assignee"] = assignee.json
-               end
-       end
+       var assignee: nullable User is writable
 end
 
 # Triggered when a user is added as a collaborator to a repository.
 class MemberEvent
        super GithubEvent
+       serialize
 
        # `User` that was added.
-       fun member: User do return new User.from_json(api, json["member"].as(JsonObject))
-
-       # Set member.
-       fun member=(member: User) do json["member"] = member.json
+       var member: User is writable
 end
 
 # Triggered when an event occurs on a pull request.
@@ -296,123 +178,65 @@ end
 # labeled, unlabeled, opened, closed, reopened, or synchronized.
 class PullRequestEvent
        super GithubEvent
+       serialize
 
        # The pull request number.
-       fun number: Int do return json["number"].as(Int)
-
-       # Set number.
-       fun number=(number: Int) do json["number"] = number
+       var number: Int is writable
 
        # The `PullRequest` itself.
-       fun pull: PullRequest do
-               return new PullRequest.from_json(api, repo, json["pull_request"].as(JsonObject))
-       end
-
-       # Set pull.
-       fun pull=(pull: PullRequest) do json["pull_request"] = pull.json
+       var pull: PullRequest is writable
 end
 
 # Triggered when a comment is created on a pull request diff.
 class PullRequestReviewCommentEvent
        super GithubEvent
+       serialize
 
        # The `Comment` itself.
-       fun comment: ReviewComment do
-               return new ReviewComment.from_json(api, repo, json["comment"].as(JsonObject))
-       end
-
-       # Set comment.
-       fun comment=(comment: ReviewComment) do json["comment"] = comment.json
+       var comment: ReviewComment is writable
 
        # `PullRequest` the `comment` belongs to.
-       fun pull: PullRequest do
-               return new PullRequest.from_json(api, repo, json["pull_request"].as(JsonObject))
-       end
-
-       # Set pull.
-       fun pull=(pull: PullRequest) do json["pull_request"] = pull.json
+       var pull: PullRequest is writable
 end
 
 # Triggered when a repository branch is pushed to.
 class PushEvent
        super GithubEvent
+       serialize
 
        # SHA of the HEAD commit on the repository.
-       fun head: String do return json["head"].as(String)
-
-       # Set head.
-       fun head=(head: String) do json["head"] = head
+       var head_commit: Commit is writable
 
        # Full Git ref that was pushed.
        #
        # Example: “refs/heads/master”
-       fun ref: String do return json["ref"].as(String)
-
-       # Set ref.
-       fun ref=(ref: String) do json["ref"] = ref
+       var ref: String is writable
 
        # Number of commits in the push.
-       fun size: Int do return json["size"].as(Int)
-
-       # Set size.
-       fun size=(size: Int) do json["size"] = size
+       var size: nullable Int is writable
 
        # Array of pushed commits.
-       fun commits: Array[Commit] do
-               var res = new Array[Commit]
-               var arr = json["commits"].as(JsonArray)
-               for obj in arr do
-                       if not obj isa JsonObject then continue
-                       res.add api.load_commit(repo, obj["sha"].as(String)).as(not null)
-               end
-               return res
-       end
-
-       # Set commits.
-       fun commits=(commits: Array[Commit]) do
-               var arr = new JsonArray
-               for commit in commits do arr.add commit.json
-               json["commits"] = arr
-       end
+       var commits = new Array[Commit] is writable, optional
 end
 
 # Triggered when the status of a Git commit changes.
 class StatusEvent
        super GithubEvent
+       serialize
 
        # The `Commit` itself.
-       fun commit: Commit do
-               return api.load_commit(repo, json["sha"].as(String)).as(not null)
-       end
-
-       # Set commit.
-       fun commit=(commit: Commit) do json["sha"] = commit.sha
+       var sha: String is writable
 
        # New state.
        #
        # Can be `pending`, `success`, `failure`, or `error`.
-       fun state: String do return json["state"].as(String)
-
-       # Set state.
-       fun state=(state: String) do json["state"] = state
+       var state: String is writable
 
        # Optional human-readable description added to the status.
-       fun description: nullable String do
-               var res = json.get_or_null("description")
-               if res isa String then return res else return null
-       end
-
-       # Set description.
-       fun description=(description: nullable String) do json["description"] = description
+       var description: nullable String is writable
 
        # Optional link added to the status.
-       fun target_url: nullable String do
-               var res = json.get_or_null("target_url")
-               if res isa String then return res else return null
-       end
-
-       # Set target_url.
-       fun target_url=(target_url: nullable String) do json["target_url"] = target_url
+       var target_url: nullable String is writable
 
        # Array of branches containing the status' SHA.
        #
@@ -420,20 +244,41 @@ class StatusEvent
        # but the SHA may or may not be the head of the branch.
        #
        # The array includes a maximum of 10 branches.
-       fun branches: Array[Branch] do
-               var res = new Array[Branch]
-               var arr = json["branches"].as(JsonArray)
-               for obj in arr do
-                       if not obj isa JsonObject then continue
-                       res.add api.load_branch(repo, obj["name"].as(String)).as(not null)
-               end
-               return res
-       end
+       var branches = new Array[Branch] is writable, optional
+end
 
-       # Set branches.
-       fun branches=(branches: Array[Commit]) do
-               var arr = new JsonArray
-               for branch in branches do arr.add branch.json
-               json["branches"] = arr
+redef class GithubDeserializer
+
+       redef fun class_name_heuristic(json_object) do
+               if json_object.has_key("action") and json_object.has_key("commit") and json_object.has_key("comment") then
+                       return "CommitCommentEvent"
+               else if json_object.has_key("ref") and json_object.has_key("master_branch") then
+                       return "CreateEvent"
+               else if json_object.has_key("ref") and json_object.has_key("ref_type") then
+                       return "DeleteEvent"
+               else if json_object.has_key("action") and json_object.has_key("sha") then
+                       return "DeploymentEvent"
+               else if json_object.has_key("action") and json_object.has_key("state") then
+                       return "DeploymentStatusEvent"
+               else if json_object.has_key("action") and json_object.has_key("forkee") then
+                       return "ForkEvent"
+               else if json_object.has_key("action") and json_object.has_key("issue") and json_object.has_key("comment") then
+                       return "IssueCommentEvent"
+               else if json_object.has_key("action") and json_object.has_key("issue") then
+                       return "IssuesEvent"
+               else if json_object.has_key("action") and json_object.has_key("member") then
+                       return "MemberEvent"
+               else if json_object.has_key("action") and json_object.has_key("number") then
+                       return "PullRequestEvent"
+               else if json_object.has_key("action") and json_object.has_key("pull") and json_object.has_key("comment") then
+                       return "PullRequestReviewCommentEvent"
+               else if json_object.has_key("head_commit") and json_object.has_key("commits") then
+                       return "PushEvent"
+               else if json_object.has_key("action") and json_object.has_key("branches") then
+                       return "StatusEvent"
+               else if json_object.has_key("action") and json_object.has_key("issue") then
+                       return "GithubEvent"
+               end
+               return super
        end
 end
index 04ae662..70f16c2 100644 (file)
@@ -37,7 +37,7 @@
 # redef class CommitCommentEvent
 #
 #    redef fun log_event(l) do
-#        print "new comment on commit {comment.commit.sha}"
+#        print "new comment on commit {comment.commit_id}"
 #    end
 # end
 #
@@ -90,33 +90,33 @@ abstract class HookListener
        # How to build events from received json objects.
        fun event_factory(kind: String, json: JsonObject): GithubEvent do
                if kind == "commit_comment" then
-                       return new CommitCommentEvent.from_json(api, json)
+                       return api.deserialize(json.to_json).as(CommitCommentEvent)
                else if kind == "create" then
-                       return new CreateEvent.from_json(api, json)
+                       return api.deserialize(json.to_json).as(CreateEvent)
                else if kind == "delete" then
-                       return new DeleteEvent.from_json(api, json)
+                       return api.deserialize(json.to_json).as(DeleteEvent)
                else if kind == "deployment" then
-                       return new DeploymentEvent.from_json(api, json)
+                       return api.deserialize(json.to_json).as(DeploymentEvent)
                else if kind == "deployment_status" then
-                       return new DeploymentStatusEvent.from_json(api, json)
+                       return api.deserialize(json.to_json).as(DeploymentStatusEvent)
                else if kind == "fork" then
-                       return new ForkEvent.from_json(api, json)
+                       return api.deserialize(json.to_json).as(ForkEvent)
                else if kind == "issues" then
-                       return new IssuesEvent.from_json(api, json)
+                       return api.deserialize(json.to_json).as(IssuesEvent)
                else if kind == "issue_comment" then
-                       return new IssueCommentEvent.from_json(api, json)
+                       return api.deserialize(json.to_json).as(IssueCommentEvent)
                else if kind == "member" then
-                       return new MemberEvent.from_json(api, json)
+                       return api.deserialize(json.to_json).as(MemberEvent)
                else if kind == "pull_request" then
-                       return new PullRequestEvent.from_json(api, json)
+                       return api.deserialize(json.to_json).as(PullRequestEvent)
                else if kind == "pull_request_review_comment" then
-                       return new PullRequestReviewCommentEvent.from_json(api, json)
+                       return api.deserialize(json.to_json).as(PullRequestReviewCommentEvent)
                else if kind == "push" then
-                       return new PushEvent.from_json(api, json)
+                       return api.deserialize(json.to_json).as(PushEvent)
                else if kind == "status" then
-                       return new StatusEvent.from_json(api, json)
+                       return api.deserialize(json.to_json).as(StatusEvent)
                else
-                       return new GithubEvent.from_json(api, json)
+                       return api.deserialize(json.to_json).as(GithubEvent)
                end
        end
 
diff --git a/lib/mongodb/queries.nit b/lib/mongodb/queries.nit
new file mode 100644 (file)
index 0000000..c7bb18f
--- /dev/null
@@ -0,0 +1,493 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Mongo queries framework
+#
+# The `queries` framework is used to build Mongo queries as JsonObject with
+# a fluent interface.
+#
+# Using the `queries` framework we can get from this:
+#
+# ~~~nitish
+# var exists = new JsonObject
+# exists["$exists"] = true
+#
+# var query = new JsonObject
+# query["login"] = "Morriar"
+# query["email"] = exists
+#
+# collection.find(query)
+# ~~~
+#
+# To this:
+#
+# ~~~nitish
+# collection.find((new MongoMatch).eq("login", "Morriar").exists("email", true))
+# ~~~
+#
+# The framework provides three classes used to map the MongoDB query API:
+# * `MongoMatch` the base query that can be used with most Mongo services
+# * `MongoPipeline` the array of queries that is expected by `MongoCollection::aggregate`
+# * `MongoGroup` the group query for a `MongoPipeline`
+#
+# More on this features can be found in the official MongoDB documentation:
+# https://docs.mongodb.com/manual/reference/operator/
+module queries
+
+import mongodb
+
+# A basic match query
+#
+# `MongoMatch` is used with most of the Mongo services like `find`, `find_all`,
+# `remove` etc.
+#
+# Building a query can be done with the fluent interface:
+#
+# ~~~
+# var query = (new MongoMatch).
+#      eq("login", "Morriar").
+#      gt("age", 18).
+#      exists("email", true).
+#      is_in("status", [1, 2, 3, 4])
+# ~~~
+#
+# Fore more help on how to use the query operators of MongoDB please
+# refer to the official MongoDB documentation:
+# https://docs.mongodb.com/manual/reference/operator/query/
+class MongoMatch
+       super JsonObject
+
+       private fun op(name: String, field: String, value: nullable Jsonable): MongoMatch do
+               var q = new JsonObject
+               q["${name}"] = value
+               self[field] = q
+               return self
+       end
+
+       # Match documents where `field` equals `value`
+       #
+       # https://docs.mongodb.com/manual/reference/operator/query/eq/#op._S_eq
+       #
+       # ~~~json
+       # {field: {$eq: value} }
+       # ~~~
+       fun eq(field: String, value: nullable Jsonable): MongoMatch do
+               self[field] = value
+               return self
+       end
+
+       # Match documents where `field` not equals `value`
+       #
+       # https://docs.mongodb.com/manual/reference/operator/query/ne/#op._S_ne
+       #
+       # ~~~json
+       # {field: {$ne: value} }
+       # ~~~
+       fun ne(field: String, value: nullable Jsonable): MongoMatch do
+               op("ne", field, value)
+               return self
+       end
+
+       # Match documents where `field` is greater than `value`
+       #
+       # https://docs.mongodb.com/manual/reference/operator/query/gt/#op._S_gt
+       #
+       # ~~~json
+       # {field: {$gt: value} }
+       # ~~~
+       fun gt(field: String, value: nullable Jsonable): MongoMatch do
+               op("gt", field, value)
+               return self
+       end
+
+       # Match documents where `field` is greater or equal to `value`
+       #
+       # https://docs.mongodb.com/manual/reference/operator/query/gte/#op._S_gte
+       #
+       # ~~~json
+       # {field: {$gte: value} }
+       # ~~~
+       fun gte(field: String, value: nullable Jsonable): MongoMatch do
+               op("gte", field, value)
+               return self
+       end
+
+       # Match documents where `field` is less than `value`
+       #
+       # https://docs.mongodb.com/manual/reference/operator/query/lt/#op._S_lt
+       #
+       # ~~~json
+       # {field: {$lt: value} }
+       # ~~~
+       fun lt(field: String, value: nullable Jsonable): MongoMatch do
+               op("lt", field, value)
+               return self
+       end
+
+       # Match documents where `field` is less or equal to `value`
+       #
+       # https://docs.mongodb.com/manual/reference/operator/query/lte/
+       #
+       # ~~~json
+       # {field: {$lte: value} }
+       # ~~~
+       fun lte(field: String, value: nullable Jsonable): MongoMatch do
+               op("lte", field, value)
+               return self
+       end
+
+       # Match documents where `field` exists or not
+       #
+       # https://docs.mongodb.com/manual/reference/operator/query/exists/#op._S_exists
+       #
+       # ~~~json
+       # {field: {$exists: boolean} }
+       # ~~~
+       #
+       # When `exists` is true, `$exists` matches the documents that contain the
+       # field, including documents where the field value is null.
+       # If <boolean> is false, the query returns only the documents that do not
+       # contain the field.
+       fun exists(field: String, exists: Bool): MongoMatch do
+               op("exists", field, exists)
+               return self
+       end
+
+       # Match documents where `field` is in `values`
+       #
+       # https://docs.mongodb.com/manual/reference/operator/query/in/
+       #
+       # ~~~json
+       # { field: { $in: [<value1>, <value2>, ... <valueN> ] } }
+       # ~~~
+       #
+       # `$in` selects the documents where the value of a field equals any value
+       # in the specified array.
+       fun is_in(field: String, values: Array[nullable Jsonable]): MongoMatch do
+               op("$in", field, new JsonArray.from(values))
+               return self
+       end
+
+       # Match documents where `field` is not in `values`
+       #
+       # https://docs.mongodb.com/manual/reference/operator/query/nin/
+       #
+       # ~~~json
+       # { field: { $nin: [<value1>, <value2>, ... <valueN> ] } }
+       # ~~~
+       #
+       # `$nin` selects the documents where:
+       # * the field value is not in the specified array or
+       # * the field does not exist.
+       fun is_nin(field: String, values: Array[nullable Jsonable]): MongoMatch do
+               op("$nin", field, new JsonArray.from(values))
+               return self
+       end
+end
+
+# Mongo pipelines are arrays of aggregation stages
+#
+# With the `MongoCollection::aggregate` method, pipeline stages appear in a array.
+# Documents pass through the stages in sequence.
+#
+# ~~~json
+# db.collection.aggregate( [ { <stage> }, ... ] )
+# ~~~
+#
+# The MongoPipeline fluent interface can be used to bluid a pipeline:
+# ~~~
+# var pipeline = (new MongoPipeline).
+#      match((new MongoMatch).eq("game", "nit")).
+#      group((new MongoGroup("$game._id")).sum("nitcoins", "$game.nitcoins")).
+#      sort((new MongoMatch).eq("nitcoins", -1)).
+#      limit(10)
+# ~~~
+#
+# The pipeline can then be used in an aggregation query:
+# ~~~nitish
+# collection.aggregate(pipeline)
+# ~~~
+#
+# For more information read about MongoDB pipeline operators from the MongoDB
+# official documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/
+class MongoPipeline
+       super JsonArray
+
+       # Add a stage to the pipeline
+       #
+       # https://docs.mongodb.com/manual/reference/operator/aggregation/#stage-operators
+       #
+       # Each stage is registered as:
+       # ~~~json
+       # { $<stage>: <json> }
+       # ~~~
+       fun add_stage(stage: String, json: Jsonable): MongoPipeline do
+               var obj = new JsonObject
+               obj["${stage}"] = json
+               add obj
+               return self
+       end
+
+       # Apply projection
+       #
+       # https://docs.mongodb.com/manual/reference/operator/aggregation/project/#pipe._S_project
+       #
+       # Passes along the documents with only the specified fields to the next stage
+       # in the pipeline.
+       #
+       # ~~~json
+       # { $project: { <specifications> } }
+       # ~~~
+       #
+       # The specified fields can be existing fields from the input documents or
+       # newly computed fields.
+       fun project(projection: JsonObject): MongoPipeline do return add_stage("project", projection)
+
+       # Apply match
+       #
+       # https://docs.mongodb.com/manual/reference/operator/aggregation/match/
+       #
+       # Filters the documents to pass only the documents that match the specified
+       # condition(s) to the next pipeline stage.
+       #
+       # ~~~json
+       # { $match: { <query> } }
+       # ~~~
+       fun match(query: MongoMatch): MongoPipeline do return add_stage("match", query)
+
+       # Apply sort
+       #
+       # https://docs.mongodb.com/manual/reference/operator/aggregation/sort/
+       #
+       # Sorts all input documents and returns them to the pipeline in sorted order.
+       #
+       # ~~~json
+       # { $sort: { <projection> } }
+       # ~~~
+       fun sort(projection: JsonObject): MongoPipeline do return add_stage("sort", projection)
+
+       # Apply skip
+       #
+       # https://docs.mongodb.com/manual/reference/operator/aggregation/skip/
+       #
+       # Skips over the specified number of documents that pass into the stage and
+       # passes the remaining documents to the next stage in the pipeline.
+       #
+       # ~~~json
+       # { $skip: { <number> } }
+       # ~~~
+       fun skip(number: Int): MongoPipeline do return add_stage("skip", number)
+
+       # Apply limit
+       #
+       # https://docs.mongodb.com/manual/reference/operator/aggregation/limit/
+       #
+       # Limits the number of documents passed to the next stage in the pipeline.
+       #
+       # ~~~json
+       # { $limit: { <number> } }
+       # ~~~
+       fun limit(number: Int): MongoPipeline do return add_stage("limit", number)
+
+       # Apply group
+       #
+       # https://docs.mongodb.com/manual/reference/operator/aggregation/group/
+       #
+       # Groups documents by some specified expression and outputs to the next stage
+       # a document for each distinct grouping.
+       #
+       # The output documents contain an `_id` field which contains the distinct
+       # group by key.
+       #
+       # The output documents can also contain computed fields that hold the values
+       # of some accumulator expression grouped by the `$group`'s `_id` field.
+       # `$group` does not order its output documents.
+       #
+       # ~~~json
+       # { $group: { <group> } }
+       # ~~~
+       fun group(group: MongoGroup): MongoPipeline do return add_stage("group", group)
+end
+
+# Mongo pipeline group stage
+#
+# https://docs.mongodb.com/manual/reference/operator/aggregation/group/#pipe._S_group
+#
+# Groups documents by some specified expression and outputs to the next stage a
+# document for each distinct grouping.
+#
+# ~~~
+# var group = (new MongoGroup("$game._id")).sum("nitcoins", "$game.nitcoins")
+#
+# var pipeline = (new MongoPipeline).group(group)
+# ~~~
+#
+# The output documents contain an `_id` field which contains the distinct group by key.
+# The output documents can also contain computed fields that hold the values of
+# some accumulator expression grouped by the `$group`‘s `_id` field.
+# `$group` does not order its output documents.
+#
+# The `$group` stage has the following prototype form:
+#
+# ~~~json
+# { $group: { _id: <expression>, <field1>: { <accumulator1> : <expression1> }, ... } }
+# ~~~
+#
+# The `_id` field is mandatory; however, you can specify an `_id` value of null
+# to calculate accumulated values for all the input documents as a whole.
+#
+# The remaining computed fields are optional and computed using the `<accumulator>`
+# operators.
+class MongoGroup
+       super JsonObject
+
+       # Group `_id`
+       #
+       # See `MongoGroup::group`.
+       var id: String
+
+       init do self["_id"] = id
+
+       # Add an accumulator
+       #
+       # Each accumulator is registered as:
+       # ~~~json
+       # <field>: { <accumulator> : <expression> }
+       # ~~~
+       private fun acc(name: String, field: String, expression: nullable Jsonable): MongoGroup do
+               var q = new JsonObject
+               q["${name}"] = expression
+               self[field] = q
+               return self
+       end
+
+       # Calculates and returns the sum of numeric values
+       #
+       # https://docs.mongodb.com/manual/reference/operator/aggregation/sum/#grp._S_sum
+       #
+       # ~~~json
+       # { $sum: <expression> }
+       # ~~~
+       #
+       # `$sum` ignores non-numeric values.
+       fun sum(field: String, expression: Jsonable): MongoGroup do
+               return acc("sum", field, expression)
+       end
+
+       # Returns the average value of the numeric values
+       #
+       # https://docs.mongodb.com/manual/reference/operator/aggregation/avg/
+       #
+       # ~~~json
+       # { $avg: <expression> }
+       # ~~~
+       #
+       # `$avg` ignores non-numeric values.
+       fun avg(field: String, expression: Jsonable): MongoGroup do
+               return acc("avg", field, expression)
+       end
+
+       # Returns the maximum value
+       #
+       # https://docs.mongodb.com/manual/reference/operator/aggregation/max/
+       #
+       # ~~~json
+       # { $max: <expression> }
+       # ~~~
+       #
+       # `$max` compares both value and type, using the specified BSON comparison
+       # order for values of different types.
+       fun max(field: String, expression: Jsonable): MongoGroup do
+               return acc("max", field, expression)
+       end
+
+       # Returns the minimum value
+       #
+       # https://docs.mongodb.com/manual/reference/operator/aggregation/min/
+       #
+       # ~~~json
+       # { $min: <expression> }
+       # ~~~
+       #
+       # `$min` compares both value and type, using the specified BSON comparison
+       # order for values of different types.
+       fun min(field: String, expression: Jsonable): MongoGroup do
+               return acc("min", field, expression)
+       end
+
+       # Return the first value
+       #
+       # https://docs.mongodb.com/manual/reference/operator/aggregation/first/
+       #
+       # ~~~json
+       # { $first: <expression> }
+       # ~~~
+       #
+       # Returns the value that results from applying an expression to the first
+       # document in a group of documents that share the same group by key.
+       #
+       # Only meaningful when documents are in a defined order.
+       fun first(field: String, expression: Jsonable): MongoGroup do
+               return acc("first", field, expression)
+       end
+
+       # Return the last value
+       #
+       # https://docs.mongodb.com/manual/reference/operator/aggregation/last/
+       #
+       # ~~~json
+       # { $last: <expression> }
+       # ~~~
+       #
+       # Returns the value that results from applying an expression to the last
+       # document in a group of documents that share the same group by key.
+       #
+       # Only meaningful when documents are in a defined order.
+       fun last(field: String, expression: Jsonable): MongoGroup do
+               return acc("last", field, expression)
+       end
+
+       # Push to an array
+       #
+       # https://docs.mongodb.com/manual/reference/operator/aggregation/push/
+       #
+       # ~~~json
+       # { $push: <expression> }
+       # ~~~
+       #
+       # Returns an array of all values that result from applying an expression to
+       # each document in a group of documents that share the same group by key.
+       fun push(field: String, expr: Jsonable): MongoGroup do
+               return acc("push", field, expr)
+       end
+
+       # Push to a unique array
+       #
+       # https://docs.mongodb.com/manual/reference/operator/aggregation/addToSet/
+       #
+       # ~~~json
+       # { $addToSet: <expression> }
+       # ~~~
+       #
+       # Returns an array of all unique values that results from applying an
+       # expression to each document in a group of documents that share the same
+       # group by key.
+       #
+       # Order of the elements in the output array is unspecified.
+       fun addToSet(field: String, expr: Jsonable): MongoGroup do
+               return acc("addToSet", field, expr)
+       end
+end
index 1833fca..8efa7d0 100644 (file)
 module http_request
 
 import core
+import serialization
 
 # A request received over HTTP, is build by `HttpRequestParser`
 class HttpRequest
+       serialize
+
        private init is old_style_init do end
 
        # HTTP protocol version
@@ -32,9 +35,6 @@ class HttpRequest
        # Method of this request (GET or POST)
        var method: String
 
-       # The host targetter by this request (usually the server)
-       var host: String
-
        # The full URL requested by the client (including the `query_string`)
        var url: String
 
index dd3e03b..ec83289 100644 (file)
 # Provides the `HttpResponse` class and `http_status_codes`
 module http_response
 
+import serialization
+
 # A response to send over HTTP
 class HttpResponse
+       serialize
 
        # HTTP protocol version
        var http_version = "HTTP/1.0" is writable
index ca37876..4ffa6d8 100644 (file)
@@ -288,7 +288,7 @@ class GithubUser
                        res.error 403
                        return
                end
-               res.json user.json
+               res.json user
        end
 end
 
index 9f9cfed..c950ea4 100644 (file)
@@ -121,7 +121,7 @@ module pop_repos
 
 import serialization
 import json::serialization
-import mongodb
+import mongodb::queries
 
 # A Repository is an object that can store serialized instances.
 #
@@ -186,12 +186,16 @@ interface RepositoryQuery end
 # Serialization from/to Json is used to translate from/to nit instances.
 #
 # See `MongoRepository` for a concrete implementation example.
-interface JsonRepository[E: Serializable]
+abstract class JsonRepository[E: Serializable]
        super Repository[E]
 
        redef fun serialize(item) do
                if item == null then return null
-               return item.serialize_to_json
+               var stream = new StringWriter
+               var serializer = new RepoSerializer(stream)
+               serializer.serialize item
+               stream.close
+               return stream.to_s
        end
 
        redef fun deserialize(string) do
@@ -201,6 +205,13 @@ interface JsonRepository[E: Serializable]
        end
 end
 
+private class RepoSerializer
+       super JsonSerializer
+
+       # Remove caching when saving refs to db
+       redef fun serialize_reference(object) do serialize object
+end
+
 # A Repository that uses MongoDB as backend.
 #
 # ~~~
@@ -307,6 +318,17 @@ class MongoRepository[E: Serializable]
        end
 
        redef fun clear do return collection.drop
+
+       # Perform an aggregation query over the repo.
+       fun aggregate(pipeline: JsonArray): Array[E] do
+               var res = new Array[E]
+               for obj in collection.aggregate(pipeline) do
+                       var instance = deserialize(obj.to_json)
+                       if instance == null then continue
+                       res.add instance
+               end
+               return res
+       end
 end
 
 # JsonObject can be used as a `RepositoryQuery`.
diff --git a/lib/trees/trie.nit b/lib/trees/trie.nit
new file mode 100644 (file)
index 0000000..73c5e0a
--- /dev/null
@@ -0,0 +1,200 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# A trie (or prefix tree) is a datastructure used to perform prefix searches.
+#
+# The trie uses an arborescent datastructure to perform searches on a prefix.
+# With this version of the trie, you can link data to nodes so the trie can
+# be used as a kind of Map by prefix.
+#
+# ~~~
+# # Associate some integers to Map keys
+# var trie = new Trie[Int]
+# trie["foo"] = 1
+# trie["fooo"] = 2
+# trie["foooo"] = 3
+# trie["bar"] = 4
+# trie["baz"] = 5
+#
+# # Get stored values by key
+# print trie.has_key("foo")
+# print trie["foo"] == 1
+#
+# # Get stored values by prefix
+# assert trie.has_prefix("fo")
+# assert not trie.has_prefix("z")
+# assert trie.find_by_prefix("foo") == [1, 2, 3]
+# assert trie.find_by_prefix("bar") == [4]
+# assert trie.find_by_prefix("z").is_empty
+# ~~~
+module trie
+
+import trees
+
+# Trie data structure for prefix searches
+#
+# ~~~
+# # Associate some integers to Map keys
+# var trie = new Trie[Int]
+# trie["foo"] = 1
+# trie["fooo"] = 2
+# trie["bar"] = 3
+#
+# # Search by key
+# assert trie.has_key("foo")
+# assert trie["foo"] == 1
+#
+# # Search by prefix
+# assert trie.find_by_prefix("") == [1, 2, 3]
+# assert trie.find_by_prefix("foo") == [1, 2]
+# assert trie.find_by_prefix("baz").is_empty
+# ~~~
+class Trie[E]
+       super Map[String, E]
+
+       # Trie root
+       #
+       # Root children are used to store the first letters.
+       private var root = new TrieNode[E]
+
+       redef fun []=(key, value) do
+               var children = root.children
+
+               for i in [0..key.length[ do
+                       var c = key.chars[i]
+
+                       var node
+                       if children.has_key(c) then
+                               node = children[c]
+                       else
+                               node = new TrieNode[E](c)
+                               children[c] = node
+                       end
+                       children = node.children
+
+                       if i == key.length - 1 then
+                               node.is_leaf = true
+                               node.value = value
+                       end
+               end
+       end
+
+       # Cache node used between `has_key` and `[]`
+       private var cache: nullable TrieNode[E] = null
+
+       # Cache key used between `has_key` and `[]`
+       private var cache_key: nullable String = null
+
+       redef fun [](key) do
+               if cache_key == key then return cache.as(not null).value.as(not null)
+               var node = search_node(key)
+               assert node != null
+               return node.value
+       end
+
+       redef fun has_key(key) do
+               var node = search_node(key)
+               if node == null then return false
+               var res = node.is_leaf
+               if res then
+                       cache = node
+                       cache_key = key.as(String)
+               end
+               return res
+       end
+
+       # Find values stored under `prefix`
+       #
+       # ~~~
+       # # Associate some integers to Map keys
+       # var trie = new Trie[Int]
+       # trie["foo"] = 1
+       # trie["fooo"] = 2
+       # trie["foooo"] = 3
+       # trie["bar"] = 4
+       #
+       # assert trie.find_by_prefix("") == [1, 2, 3, 4]
+       # assert trie.find_by_prefix("foo") == [1, 2, 3]
+       # assert trie.find_by_prefix("bar") == [4]
+       # assert trie.find_by_prefix("baz").is_empty
+       # ~~~
+       fun find_by_prefix(prefix: String): Array[E] do
+               var res = new Array[E]
+               var node
+               if prefix == "" then
+                       node = root
+               else
+                       node = search_node(prefix)
+               end
+               if node != null then
+                       node.collect_values(res)
+               end
+               return res
+       end
+
+       # Find values stored under `prefix`
+       #
+       # ~~~
+       # # Associate some integers to Map keys
+       # var trie = new Trie[Int]
+       # trie["foo"] = 1
+       # trie["bar"] = 4
+       # trie["baz"] = 5
+       #
+       # assert trie.has_prefix("")
+       # assert trie.has_prefix("f")
+       # assert not trie.has_prefix("z")
+       # ~~~
+       fun has_prefix(prefix: String): Bool do
+               if prefix == "" then return true
+               return search_node(prefix) != null
+       end
+
+       # Search a node by a key or prefix
+       #
+       # Returns `null` if no node matches `string`.
+       private fun search_node(string: nullable Object): nullable TrieNode[E] do
+               if string == null then return null
+               string = string.to_s
+               var children = root.children
+               var node = null
+               for i in [0..string.length[ do
+                       var c = string.chars[i]
+                       if children.has_key(c) then
+                               node = children[c]
+                               children = node.children
+                       else
+                               node = null
+                               break
+                       end
+               end
+               return node
+       end
+end
+
+# TrieNode used to store the Character key of the value
+private class TrieNode[E]
+       var c: nullable Char
+       var value: nullable E
+       var children = new HashMap[Char, TrieNode[E]]
+       var is_leaf: Bool = false
+
+       fun collect_values(values: Array[E]) do
+               var value = self.value
+               if value != null then values.add value
+               for child in children.values do
+                       child.collect_values(values)
+               end
+       end
+end
index 5e9ec4d..14767cd 100644 (file)
@@ -1,6 +1,6 @@
 # NAME
 
-nitls - lists the packages, groups and paths of Nit sources files.
+nitls - search and lists the packages, groups and paths of Nit sources files.
 
 # SYNOPSIS
 
@@ -8,9 +8,9 @@ nitls [*options*] [*FILE*]...
 
 # DESCRIPTION
 
-`nitls` is used to list Nit files in directories and extract the module-group-package relation.
+`nitls` is used to search and list Nit files in directories and extract the module-group-package relation.
 
-It is basically a `ls` or a simple `find` specialized on `.nit` source files.
+It is basically a `ls`, a `which` or a simple `find` specialized on `.nit` source files.
 
 By default `nitls` works with the current directory (`.`).
 
@@ -29,11 +29,43 @@ Each file can then be:
 
 Show the tree of modules from the current directory.
 
-    $ nitls -t
+    $ nitls
+    test_prog: Test program for model tools. (.)
+    |--game: Gaming group (./game)
+    |  `--game: A game abstraction for RPG. (./game/game.nit)
+    |--platform: Fictive Crappy Platform. (./platform)
+    |  `--platform: Declares base types allowed on the platform. (./platform/platform.nit)
+    |--rpg: Role Playing Game group (./rpg)
+    |  |--careers: Careers of the game. (./rpg/careers.nit)
+    |  |--character: Characters are playable entity in the world. (./rpg/character.nit)
+    |  |--combat: COmbat interactions between characters. (./rpg/combat.nit)
+    |  |--races: Races of the game. (./rpg/races.nit)
+    |  `--rpg: A worlg RPG abstraction. (./rpg/rpg.nit)
+    `--test_prog: A test program with a fake model to check model tools. (./test_prog.nit)
+
 
 Show the list of packages imported by the modules of the current directory.
 
     $ nitls -d -P *.nit
+    base64: Offers the base 64 encoding and decoding algorithms (/home/privat/prog/nit/lib/base64.nit)
+    core: # Nit common library of core classes and methods (/home/privat/prog/nit/lib/core)
+    curl: Data transfer with URL syntax (/home/privat/prog/nit/lib/curl)
+    json (/home/privat/prog/nit/lib/json)
+    libevent: Low-level wrapper around the libevent library to manage events on file descriptors (/home/privat/prog/nit/lib/libevent.nit)
+    md5: Native MD5 digest implementation as `Text::md5` (/home/privat/prog/nit/lib/md5.nit)
+    more_collections: Highly specific, but useful, collections-related classes. (/home/privat/prog/nit/lib/more_collections.nit)
+    nitcc_runtime: Runtime library required by parsers and lexers generated by nitcc (/home/privat/prog/nit/lib/nitcc_runtime.nit)
+    nitcorn: Lightweight framework for Web applications development (.)
+    parser_base: Simple base for hand-made parsers of all kinds (/home/privat/prog/nit/lib/parser_base.nit)
+    performance_analysis: Services to gather information on the performance of events by categories (/home/privat/prog/nit/lib/performance_analysis.nit)
+    realtime: Services to keep time of the wall clock time (/home/privat/prog/nit/lib/realtime.nit)
+    serialization: # Abstract serialization services (/home/privat/prog/nit/lib/serialization)
+    template: Basic template system (/home/privat/prog/nit/lib/template)
+
+Show the directory of the package `inkscape_tools`.
+
+    $ nitls -pP inkscape_tools
+    /home/privat/prog/nit/contrib/inkscape_tools
 
 # OPTIONS
 
diff --git a/share/nitweb/directives/user/user-menu.html b/share/nitweb/directives/user/user-menu.html
new file mode 100644 (file)
index 0000000..8d1f7bf
--- /dev/null
@@ -0,0 +1,19 @@
+<ul class='nav navbar-nav'>
+       <li ng-if='!user'>
+               <a href='/login'>
+                       Login
+                       <span class='octicon octicon-mark-github'></span>
+               </a>
+       </li>
+       <li class="dropdown" ng-if='user'>
+               <a class="btn btn-link dropdown-toggle" type="button" data-toggle="dropdown">
+                       {{user.login}}
+                       <img class='avatar avatar-icon' ng-src='{{user.avatar_url}}' />
+               </a>
+               <ul class="dropdown-menu dropdown-menu-right">
+                       <li><a href="/user">Profile</a></li>
+                       <li role="separator" class="divider"></li>
+                       <li><a href="/logout">Logout</a></li>
+               </ul>
+       </li>
+</ul>
index b936f1d..ffd2733 100644 (file)
@@ -14,6 +14,9 @@
                <link href='//cdnjs.cloudflare.com/ajax/libs/angular-loading-bar/0.9.0/loading-bar.min.css'
                        type='text/css' rel='stylesheet' media='all'>
 
+               <link href='https://cdnjs.cloudflare.com/ajax/libs/octicons/3.5.0/octicons.css'
+                       type='text/css' rel='stylesheet'>
+
 
                <link href='/stylesheets/nitweb_bootstrap.css' rel='stylesheet'>
                <link href='/stylesheets/nitweb.css' rel='stylesheet'>
@@ -47,6 +50,9 @@
                                                </div>
                                        </form>
                                </div>
+                               <div class='col-xs-2'>
+                                       <user-menu />
+                               </div>
                        </div>
                </nav>
                <div ng-view></div>
@@ -74,5 +80,6 @@
                <script src='/javascripts/index.js'></script>
                <script src='/javascripts/docdown.js'></script>
                <script src='/javascripts/metrics.js'></script>
+               <script src='/javascripts/users.js'></script>
        </body>
 </html>
index cd127ed..8ec516c 100644 (file)
                                }
                        }
                }])
+
+               .factory('User', [ '$http', function($http) {
+                       return {
+                               loadUser: function(cb, cbErr) {
+                                       $http.get(apiUrl + '/user')
+                                               .success(cb)
+                                               .error(cbErr);
+                               }
+                       }
+               }])
 })();
index 9dac463..21df4bf 100644 (file)
@@ -15,7 +15,7 @@
  */
 
 (function() {
-       angular.module('nitweb', ['ngRoute', 'ngSanitize', 'angular-loading-bar', 'entities', 'docdown', 'index', 'metrics'])
+       angular.module('nitweb', ['ngRoute', 'ngSanitize', 'angular-loading-bar', 'entities', 'docdown', 'index', 'metrics', 'users'])
        .config(['cfpLoadingBarProvider', function(cfpLoadingBarProvider) {
                cfpLoadingBarProvider.includeSpinner = false;
        }])
                                controller: 'IndexCtrl',
                                controllerAs: 'indexCtrl'
                        })
+                       .when('/user', {
+                               templateUrl: 'views/user.html',
+                               controller: 'UserCtrl',
+                               controllerAs: 'userCtrl'
+                       })
                        .when('/docdown', {
                                templateUrl: 'views/docdown.html',
                                controller: 'DocdownCtrl',
                                controllerAs: 'docdownCtrl'
                        })
+                       .when('/login', {
+                               controller : function(){
+                                       window.location.replace('/login');
+                               },
+                           template : "<div></div>"
+                       })
+                       .when('/logout', {
+                               controller : function(){
+                                       window.location.replace('/logout');
+                               },
+                           template : "<div></div>"
+                       })
                        .when('/doc/:id', {
                                templateUrl: 'views/doc.html',
                                controller: 'EntityCtrl',
diff --git a/share/nitweb/javascripts/users.js b/share/nitweb/javascripts/users.js
new file mode 100644 (file)
index 0000000..42e7d6f
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+(function() {
+       angular
+               .module('users', ['ngSanitize', 'model'])
+
+               .controller('UserCtrl', ['User', '$routeParams', '$scope', function(User, $routeParams, $scope) {
+                       this.loadUser = function() {
+                               User.loadUser(
+                                       function(data) {
+                                               $scope.user = data;
+                                       }, function(err) {
+                                               $scope.error = err;
+                                       });
+                       };
+
+                       this.loadUser();
+               }])
+
+               .directive('userMenu', ['User', function(User) {
+                       return {
+                               restrict: 'E',
+                               templateUrl: '/directives/user/user-menu.html',
+                               link: function ($scope, element, attrs) {
+                                       $scope.loadUser = function() {
+                                               User.loadUser(
+                                                       function(data) {
+                                                               $scope.user = data;
+                                                       }, function(err) {
+                                                               //$scope.error = err;
+                                                       });
+                                       }
+                                       $scope.loadUser();
+                               }
+                       };
+               }])
+})();
index 0fcff6c..0fd2db9 100644 (file)
@@ -252,6 +252,11 @@ entity-list:hover .btn-filter {
        border-radius: 2px;
 }
 
+.avatar-icon {
+       width: 18px;
+       height: 18px;
+}
+
 /*
  * Code Highlighting
  */
diff --git a/share/nitweb/views/user.html b/share/nitweb/views/user.html
new file mode 100644 (file)
index 0000000..b044c9c
--- /dev/null
@@ -0,0 +1,20 @@
+<div class='container'>
+       <div class='col-xs-4'>
+               <img class='avatar' width='100%' src='{{user.avatar_url}}' />
+               <h1>{{user.login}}</h1>
+               <h3>{{user.name}}</h3>
+               <ul class='list-unstyled'>
+                       <li>
+                               <span class='glyphicon glyphicon-envelope' />
+                               <a href='mailto:{{user.email}}'>{{user.email}}</a>
+                       </li>
+                       <li>
+                               <span class='glyphicon glyphicon-link' />
+                               <a href='{{user.blog}}'>{{user.blog}}</a>
+                       </li>
+               </ul>
+       </div>
+       <div class='col-xs-8'>
+               Nothing to display yet.
+       </div>
+<div>
index 9376172..9484425 100644 (file)
 module nitweb
 
 import popcorn::pop_config
+import popcorn::pop_auth
 import frontend
 import web
 
+redef class NitwebConfig
+
+       # Github client id used for Github OAuth login.
+       #
+       # * key: `github.client_id`
+       # * default: ``
+       var github_client_id: String is lazy do return value_or_default("github.client.id", "")
+
+       # Github client secret used for Github OAuth login.
+       #
+       # * key: `github.client_secret`
+       # * default: ``
+       var github_client_secret: String is lazy do
+               return value_or_default("github.client.secret", "")
+       end
+end
+
 redef class ToolContext
 
        # Path to app config file.
@@ -88,8 +106,12 @@ private class NitwebPhase
 
                var app = new App
 
+               app.use_before("/*", new SessionInit)
                app.use_before("/*", new RequestClock)
                app.use("/api", new NitwebAPIRouter(config, catalog))
+               app.use("/login", new GithubLogin(config.github_client_id))
+               app.use("/oauth", new GithubOAuthCallBack(config.github_client_id, config.github_client_secret))
+               app.use("/logout", new GithubLogout)
                app.use("/*", new StaticHandler(toolcontext.share_dir / "nitweb", "index.html"))
                app.use_after("/*", new ConsoleLog)
 
@@ -119,6 +141,7 @@ class NitwebAPIRouter
                use("/graph/", new APIGraphRouter(config))
                use("/docdown/", new APIDocdown(config))
                use("/metrics/", new APIMetricsRouter(config))
+               use("/user", new GithubUser)
        end
 end