Merge: Nitcc custom lexer
authorJean Privat <jean@pryen.org>
Mon, 15 Aug 2016 19:19:16 +0000 (15:19 -0400)
committerJean Privat <jean@pryen.org>
Mon, 15 Aug 2016 19:19:16 +0000 (15:19 -0400)
Proof of concept of how a nitcc lexer can be monkey-patched.

maybe not enough for @ppepos, but it's something.

Pull-Request: #2258
Reviewed-by: Lucas Bajolet <r4pass@hotmail.com>

59 files changed:
contrib/benitlux/src/client/client.nit
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
contrib/shibuqam/examples/reloadgame.nit [new file with mode: 0644]
contrib/shibuqam/package.ini [new file with mode: 0644]
contrib/shibuqam/shibuqam.nit [new file with mode: 0644]
examples/calculator/Makefile
examples/calculator/android/res/values/color.xml [new file with mode: 0644]
examples/calculator/org.nitlanguage.scientific_calculator.txt [new file with mode: 0644]
examples/calculator/package.ini
examples/calculator/src/android14.nit [new file with mode: 0644]
examples/calculator/src/android21/android/res/values/styles.xml [new file with mode: 0644]
examples/calculator/src/android21/android21.nit [new file with mode: 0644]
examples/calculator/src/android_calculator.nit [deleted file]
lib/binary/serialization.nit
lib/core/file.nit
lib/github/api.nit
lib/github/cache.nit
lib/github/events.nit
lib/github/hooks.nit
lib/json/serialization.nit
lib/mongodb/queries.nit [new file with mode: 0644]
lib/nitcorn/http_request.nit
lib/nitcorn/http_response.nit
lib/nitcorn/nitcorn.nit
lib/nitcorn/reactor.nit
lib/nitcorn/server_config.nit
lib/popcorn/pop_auth.nit
lib/popcorn/pop_repos.nit
lib/serialization/README.md
lib/serialization/serialization.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/frontend/serialization_phase.nit
src/nitweb.nit
src/web/api_catalog.nit
src/web/api_docdown.nit
src/web/api_feedback.nit
src/web/api_graph.nit
src/web/api_metrics.nit
src/web/api_model.nit
src/web/web_base.nit
tests/sav/nitce/test_binary_deserialization_alt1.res
tests/sav/nitce/test_json_deserialization_alt1.res
tests/sav/nitce/test_json_deserialization_alt3.res
tests/sav/nitweb.res
wallet [new file with mode: 0755]

index e2fc2a9..75ba6a9 100644 (file)
@@ -30,11 +30,6 @@ import user_views
 redef class Deserializer
        redef fun deserialize_class(name)
        do
-               if name == "Array[Beer]" then return new Array[Beer].from_deserializer(self)
-               if name == "Array[User]" then return new Array[User].from_deserializer(self)
-               if name == "Array[BeerBadge]" then return new Array[BeerBadge].from_deserializer(self)
-               if name == "Array[BeerAndRatings]" then return new Array[BeerAndRatings].from_deserializer(self)
-               if name == "Array[String]" then return new Array[String].from_deserializer(self)
                if name == "Array[UserAndFollowing]" then return new Array[UserAndFollowing].from_deserializer(self)
                return super
        end
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
diff --git a/contrib/shibuqam/examples/reloadgame.nit b/contrib/shibuqam/examples/reloadgame.nit
new file mode 100644 (file)
index 0000000..24eecf5
--- /dev/null
@@ -0,0 +1,115 @@
+# 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.
+
+# Example that uses `shibuqam` to authenticate users and count the number of time they reload.
+module reloadgame
+
+import popcorn
+import counter
+import shibuqam
+
+redef class User
+       # How many reload?
+       var seen = 0
+end
+
+# Ugly global class to track the knowledge.
+class DB
+       # All known users
+       var users = new HashMap[String, User]
+end
+# Ugly global instance to track the knowledge.
+fun db: DB do return once new DB
+
+redef class HttpRequest
+       # Like `user` but reuse an user if already seen
+       var reuser: nullable User is lazy do
+               var user = self.user
+               if user == null then return null
+
+               var saved = db.users.get_or_null(user.id)
+               if saved != null then return saved
+
+               db.users[user.id] = user
+               return user
+       end
+end
+
+# The only handler of the example.
+class ReloadGame
+       super Handler
+
+       redef fun get(http_request, response)
+       do
+               var body = """
+                       <!DOCTYPE html>
+                       <head>
+                       <meta charset="utf-8">
+                       <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
+                       <title>Nitcorn on Shibboleth/UQAM</title>
+                       </head>
+                       <body>
+                       <div class="container">
+                       <h1>Nitcorn on Shibboleth/UQAM</h1>
+               """
+
+               var user = http_request.user
+
+               if user != null then
+                       user.seen += 1
+
+                       body += """
+                       <p>Welcome {{{user.given_name}}}</p>
+                       <ul>
+                       <li>Full Name: {{{user.display_name}}}</li>
+                       <li>E-Mail: {{{user.email}}}</li>
+                       <li>Id: {{{user.id}}}</li>
+                       <li>Score: {{{user.seen}}}</li>
+                       </ul>
+                       """
+
+                       #for k, v in http_request.header do body += "<li>{k}: {v}</li>"
+               else
+                       # The login page, at the location the reverse proxy is expected to be configured
+                       # to force an authentication.
+                       var login = "/securep/login"
+                       body += """
+                       <p>Welcome annonymous, please <a href="{{{login}}}">log in</a>.</p>
+                       """
+               end
+
+               var score = new Counter[User]
+               for u in db.users.values do
+                       score[u] = u.seen
+               end
+
+               body += "<h2>Scoreboard</h2><ul>"
+               for u in score.sort.reversed do
+                       body += "<li><img src='{u.avatar}'> {u.display_name}: {u.seen}</li>"
+               end
+
+
+               body += """</ul>
+                       </div>
+                       </body>
+                       </html>
+               """
+
+               response.html body
+       end
+end
+
+var app = new App
+app.use("/*", new ReloadGame)
+app.listen("localhost", 3000)
diff --git a/contrib/shibuqam/package.ini b/contrib/shibuqam/package.ini
new file mode 100644 (file)
index 0000000..8c619ad
--- /dev/null
@@ -0,0 +1,11 @@
+[package]
+name=shibuqam
+tags=web
+maintainer=Jean Privat <jean@pryen.org>
+license=Apache-2.0
+[upstream]
+browse=https://github.com/nitlang/nit/tree/master/contrib/shibuqam/
+git=https://github.com/nitlang/nit.git
+git.directory=contrib/shibuqam/
+homepage=http://nitlanguage.org
+issues=https://github.com/nitlang/nit/issues
diff --git a/contrib/shibuqam/shibuqam.nit b/contrib/shibuqam/shibuqam.nit
new file mode 100644 (file)
index 0000000..e8da9a5
--- /dev/null
@@ -0,0 +1,69 @@
+# 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.
+
+# Gather the authenticated users on UQAM websites.
+#
+# The main method to use is `HttpRequest::user` and it extracts the information
+# of the authenticated user from the request header.
+# The real authentication must be done by a mandatory reverse proxy server.
+module shibuqam
+
+import nitcorn
+private import md5
+
+# Information on a user from Shibboleth/UQAM
+class User
+       # The *code permanent* (or the uid for non student)
+       var id: String
+
+       # Usually the first name
+       var given_name: String
+
+       # Usually "FamilyName, FirstName"
+       var display_name: String
+
+       # The email @courrier.uqam.ca (or @uqam.ca for non student)
+       var email: String
+
+       # The Gravatar URL (based on `email`)
+       var avatar: String is lazy do
+               var md5 = email.md5
+               return "https://www.gravatar.com/avatar/{md5}?d=retro"
+       end
+end
+
+redef class HttpRequest
+       # Extract the Shibboleth/UQAM information from the header, if any.
+       #
+       # We assume that a reverse proxy does the authentication and fill the request header.
+       # If the server is accessible directly, these headers can be easily forged.
+       # Thus, we assume that the reverse proxy is not by-passable.
+       #
+       # The reverse proxy might choose to force an authentication or not.
+       # If there is no authentication, there is no information in the request header (or with the `(null)` value).
+       # In this case, `null` is returned by this function.
+       fun user: nullable User do
+               var user = header.get_or_null("Remote-User")
+               if user == null or user == "(null)" then return null
+
+               var display_name = header.get_or_null("User-Display-Name")
+               var given_name = header.get_or_null("User-Given-Name")
+               var email = header.get_or_null("User-Mail")
+
+               if display_name == null or given_name == null or email == null then return null
+
+               var res = new User(user, given_name, display_name, email)
+               return res
+       end
+end
index bf2a19e..21d74f8 100644 (file)
@@ -13,34 +13,49 @@ bin/scientific: $(shell ${NITLS} -M scientific linux) ${NITC}
 
 # ---
 # Android
+#
+# There are 4 versions, combining 2 variations:
+# * scientific vs non-scientific
+# * android API 21+ vs under 21
 
-android: bin/calculator.apk bin/scientific.apk
+android: bin/calculator14.apk bin/scientific14.apk bin/calculator21.apk bin/scientific21.apk
 
-bin/calculator.apk: $(shell ${NITLS} -M src/android_calculator.nit) ${NITC} android/res/drawable-hdpi/icon.png
+bin/calculator14.apk: $(shell ${NITLS} -M src/android14.nit) ${NITC} android/res/drawable-hdpi/icon.png
        mkdir -p bin
-       ${NITC} -o $@ src/android_calculator.nit -D debug
+       ${NITC} -o $@ src/android14.nit -D debug
 
-bin/scientific.apk: $(shell ${NITLS} -M src/scientific src/android_calculator.nit) ${NITC} src/scientific/android/res/drawable-hdpi/icon.png
+bin/calculator21.apk: $(shell ${NITLS} -M src/android21) ${NITC} android/res/drawable-hdpi/icon.png
        mkdir -p bin
-       ${NITC} -o $@ src/scientific -m src/android_calculator.nit -D debug
+       ${NITC} -o $@ src/android21 -D debug
 
-android-release: $(shell ${NITLS} -M src/scientific src/android_calculator.nit) ${NITC} android/res/drawable-hdpi/icon.png
+bin/scientific14.apk: $(shell ${NITLS} -M src/scientific src/android14.nit) ${NITC} src/scientific/android/res/drawable-hdpi/icon.png
        mkdir -p bin
-       ${NITC} -o bin/calculator.apk src/scientific -m src/android_calculator.nit --release
+       ${NITC} -o $@ src/scientific -m src/android14.nit -D debug
+
+bin/scientific21.apk: $(shell ${NITLS} -M src/scientific src/android21) ${NITC} src/scientific/android/res/drawable-hdpi/icon.png
+       mkdir -p bin
+       ${NITC} -o $@ src/scientific -m src/android21 -D debug
+
+android-release: $(shell ${NITLS} -M src/scientific src/android14.nit) ${NITC} android/res/drawable-hdpi/icon.png
+       mkdir -p bin
+       ${NITC} -o bin/calculator14.apk src/android14.nit --release
+       ${NITC} -o bin/calculator21.apk src/android21 --release
+       ${NITC} -o bin/scientific14.apk src/scientific -m src/android14.nit --release
+       ${NITC} -o bin/scientific21.apk src/scientific -m src/android21 --release
 
 android/res/drawable-hdpi/icon.png: art/icon.svg ../../contrib/inkscape_tools/bin/svg_to_icons
        mkdir -p android/res
        ../../contrib/inkscape_tools/bin/svg_to_icons art/icon.svg --android --out android/res/
 
-src/scientific/android/res/drawable-hdpi/icon.png: art/icon_sci.svg ../../contrib/inkscape_tools/bin/svg_to_icons
+src/scientific/android/res/drawable-hdpi/icon.png: art/icon-sci.svg ../../contrib/inkscape_tools/bin/svg_to_icons
        mkdir -p src/scientific/android/res
        ../../contrib/inkscape_tools/bin/svg_to_icons art/icon-sci.svg --android --out src/scientific/android/res/
 
 ../../contrib/inkscape_tools/bin/svg_to_icons:
        make -C ../../contrib/inkscape_tools/
 
-android-install: bin/calculator.apk
-       adb install -r bin/calculator.apk
+android-install: bin/calculator14.apk
+       adb install -r bin/calculator14.apk
 
 # ---
 # iOS
diff --git a/examples/calculator/android/res/values/color.xml b/examples/calculator/android/res/values/color.xml
new file mode 100644 (file)
index 0000000..3ed5b06
--- /dev/null
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2014 The Android Open Source Project
+
+  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.
+  -->
+
+<resources>
+
+    <!-- Default background color for the status bar. -->
+    <color name="calculator_accent_color">#00BCD4</color>
+
+    <!-- Color to indicate an error has occured. -->
+    <color name="calculator_error_color">#F40056</color>
+
+    <!-- Background color of the calculator display. -->
+    <color name="display_background_color">#FFF</color>
+
+    <!-- Text color for the formula in the calculator display. -->
+    <color name="display_formula_text_color">#8A000000</color>
+
+    <!-- Text color for the result in the calculator display. -->
+    <color name="display_result_text_color">#6C000000</color>
+
+    <!-- Background color for the numeric pad. -->
+    <color name="pad_numeric_background_color">#434343</color>
+
+    <!-- Background color for the operator pad. -->
+    <color name="pad_operator_background_color">#636363</color>
+
+    <!-- Background color for the advanced pad. -->
+    <color name="pad_advanced_background_color">#1DE9B6</color>
+
+    <!-- Text color for a button in a pad. -->
+    <color name="pad_button_text_color">#FFF</color>
+
+    <!-- Text color for a button in the advanced pad. -->
+    <color name="pad_button_advanced_text_color">#91000000</color>
+
+    <!-- Ripple color when a button is pressed in a pad. -->
+    <color name="pad_button_ripple_color">#33FFFFFF</color>
+
+    <!-- Ripple color when a button is pressed in a pad. -->
+    <color name="pad_button_advanced_ripple_color">#1A000000</color>
+
+</resources>
diff --git a/examples/calculator/org.nitlanguage.scientific_calculator.txt b/examples/calculator/org.nitlanguage.scientific_calculator.txt
new file mode 100644 (file)
index 0000000..0ec8577
--- /dev/null
@@ -0,0 +1,10 @@
+Categories:Nit
+License:Apache2
+Web Site:http://nitlanguage.org
+Source Code:http://nitlanguage.org/nit.git/tree/HEAD:/examples/calculator
+Issue Tracker:https://github.com/nitlang/nit/issues
+
+Summary:A Scientific Calculator
+Description:
+10 digits, 16 operations, hours of fun.
+.
index 8237327..014014d 100644 (file)
@@ -9,4 +9,4 @@ git=https://github.com/nitlang/nit.git
 git.directory=examples/calculator/
 homepage=http://nitlanguage.org
 issues=https://github.com/nitlang/nit/issues
-apk=http://nitlanguage.org/fdroid/apk/calculator.apk
+apk=http://nitlanguage.org/fdroid/apk/calculator21.apk
diff --git a/examples/calculator/src/android14.nit b/examples/calculator/src/android14.nit
new file mode 100644 (file)
index 0000000..8695c3b
--- /dev/null
@@ -0,0 +1,64 @@
+# 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.
+
+# Aesthetic adaptations for Android for API 14+
+module android14
+
+import calculator
+import android
+
+redef class Button
+       init do set_android_style(native, app.native_activity,
+                                 (text or else "?").is_int,
+                                 ["+","-","×","C","÷","=","."].has(text))
+
+       # Set color and text style
+       private fun set_android_style(java_button: NativeButton, activity: NativeActivity,
+               is_number: Bool, is_basic_op: Bool)
+       in "Java" `{
+               // Set color
+               int back_color_id = 0;
+               if (is_number)
+                       back_color_id = R.color.pad_numeric_background_color;
+               else if (is_basic_op)
+                       back_color_id = R.color.pad_operator_background_color;
+               else {
+                       back_color_id = R.color.pad_advanced_background_color;
+
+                       int text_color = activity.getResources().getColor(R.color.pad_button_advanced_text_color);
+                       java_button.setTextColor(text_color);
+               }
+               java_button.setBackgroundResource(back_color_id);
+
+               // Center label, use lowercase and make text bigger
+               java_button.setGravity(android.view.Gravity.CENTER);
+               java_button.setAllCaps(false);
+               java_button.setTextSize(android.util.TypedValue.COMPLEX_UNIT_FRACTION, 100.0f);
+       `}
+end
+
+redef class TextInput
+       init do set_android_style(native, app.native_activity)
+
+       # Set text style and hide cursor
+       private fun set_android_style(java_edit_text: NativeEditText, activity: NativeActivity)
+       in "Java" `{
+               java_edit_text.setBackgroundResource(R.color.display_background_color);
+               java_edit_text.setTextColor(
+                       activity.getResources().getColor(R.color.display_formula_text_color));
+               java_edit_text.setTextSize(android.util.TypedValue.COMPLEX_UNIT_FRACTION, 120.0f);
+               java_edit_text.setCursorVisible(false);
+               java_edit_text.setGravity(android.view.Gravity.CENTER_VERTICAL | android.view.Gravity.END);
+       `}
+end
diff --git a/examples/calculator/src/android21/android/res/values/styles.xml b/examples/calculator/src/android21/android/res/values/styles.xml
new file mode 100644 (file)
index 0000000..95c844a
--- /dev/null
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2014 The Android Open Source Project
+
+  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.
+  -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <style name="CalculatorTheme" parent="@android:style/Theme.Material.Light.NoActionBar">
+        <item name="android:colorPrimary">@color/calculator_accent_color</item>
+        <item name="android:navigationBarColor">@color/calculator_accent_color</item>
+        <item name="android:statusBarColor">@color/calculator_accent_color</item>
+        <item name="android:windowContentOverlay">@null</item>
+    </style>
+
+    <style name="DisplayEditTextStyle" parent="@android:style/Widget.Material.Light.EditText">
+        <item name="android:background">@android:color/transparent</item>
+        <item name="android:cursorVisible">false</item>
+        <item name="android:fontFamily">sans-serif-light</item>
+        <item name="android:includeFontPadding">false</item>
+        <item name="android:gravity">bottom|end</item>
+    </style>
+
+    <style name="PadButtonStyle" parent="@android:style/Widget.Material.Light.Button.Borderless">
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:fontFamily">sans-serif-light</item>
+        <item name="android:gravity">center</item>
+        <item name="android:includeFontPadding">false</item>
+        <item name="android:minWidth">0dip</item>
+        <item name="android:minHeight">0dip</item>
+        <item name="android:textAllCaps">false</item>
+        <item name="android:textColor">@color/pad_button_text_color</item>
+    </style>
+
+    <style name="PadLayoutStyle">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">match_parent</item>
+    </style>
+</resources>
diff --git a/examples/calculator/src/android21/android21.nit b/examples/calculator/src/android21/android21.nit
new file mode 100644 (file)
index 0000000..36d1025
--- /dev/null
@@ -0,0 +1,50 @@
+# 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.
+
+# Aesthetic adaptations for Android Lollypop (API 21)
+module android21 is
+       android_api_min 21
+       android_api_target 21
+       android_manifest_activity """android:theme="@style/CalculatorTheme" """
+       app_files
+end
+
+import android14
+
+redef class TextInput
+       init do
+               set_android_style(native, app.native_activity)
+               super
+       end
+
+       # Deactivate the virtual keyboard and set the text style from XML resources
+       private fun set_android_style(java_edit_text: NativeEditText, activity: NativeActivity)
+       in "Java" `{
+               java_edit_text.setShowSoftInputOnFocus(false);
+               java_edit_text.setTextAppearance(activity, R.style.DisplayEditTextStyle);
+       `}
+end
+
+redef class Button
+       init do
+               set_text_style(native, app.native_activity)
+               super
+       end
+
+       # Set the text style from XML resources
+       private fun set_text_style(java_button: NativeButton, activity: NativeActivity)
+       in "Java" `{
+               java_button.setTextAppearance(activity, R.style.PadButtonStyle);
+       `}
+end
diff --git a/examples/calculator/src/android_calculator.nit b/examples/calculator/src/android_calculator.nit
deleted file mode 100644 (file)
index 1d51073..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-# 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.
-
-# Aesthetic adaptations for Android
-module android_calculator
-
-import calculator
-import android
-
-redef class Button
-       init do set_android_style(native, (text or else "?").is_int)
-
-       private fun set_android_style(java_button: NativeButton, is_number: Bool)
-       in "Java" `{
-               // Flatten the background and use a different color for digit buttons
-               int color = is_number? android.graphics.Color.DKGRAY: android.graphics.Color.TRANSPARENT;
-               java_button.setBackgroundColor(color);
-
-               // Center the label on both horizontal and vertical axes
-               java_button.setGravity(android.view.Gravity.CENTER);
-
-               // Set lowercase text to correctly display constants like e and π
-               java_button.setAllCaps(false);
-       `}
-end
index a530ebf..fff7fb5 100644 (file)
@@ -153,7 +153,7 @@ class BinaryDeserializer
                return new Couple[String, nullable Object](next_attribute_name, next_object)
        end
 
-       redef fun deserialize_attribute(name)
+       redef fun deserialize_attribute(name, static_type)
        do
                if unclaimed_attributes.last.keys.has(name) then
                        # Pick in already deserialized attributes
index bea9672..79ddf45 100644 (file)
@@ -626,6 +626,22 @@ class Path
                return res
        end
 
+       # Correctly join `self` with `subpath` using the directory separator.
+       #
+       # Using a standard "{self}/{path}" does not work in the following cases:
+       #
+       # * `self` is empty.
+       # * `path` starts with `'/'`.
+       #
+       # This method ensures that the join is valid.
+       #
+       #     var hello = "hello".to_path
+       #     assert (hello/"world").to_s   == "hello/world"
+       #     assert ("hel/lo".to_path / "wor/ld").to_s == "hel/lo/wor/ld"
+       #     assert ("".to_path / "world").to_s == "world"
+       #     assert (hello / "/world").to_s  == "/world"
+       #     assert ("hello/".to_path / "world").to_s  == "hello/world"
+       fun /(subpath: String): Path do return new Path(path / subpath)
 
        # Lists the files contained within the directory at `path`.
        #
@@ -657,7 +673,7 @@ class Path
                        end
                        var name = de.to_s_with_copy
                        if name == "." or name == ".." then continue
-                       res.add new Path(path / name)
+                       res.add self / name
                end
                d.closedir
 
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
 
index 6a66407..8861e19 100644 (file)
 #}"""
 # ~~~
 #
-# ## JSON to Nit objects
+# ## Read JSON to create Nit objects
 #
-# The `JsonDeserializer` support reading JSON code with minimal metadata
-# to easily create Nit object from client-side code or configuration files.
-# Each JSON object must define the `__class` attribute with the corresponding
-# Nit class and the expected attributes with its name in Nit followed by its value.
+# The `JsonDeserializer` supports reading JSON code with or without metadata.
+# It can create Nit objects from a remote service returning JSON data
+# or to read local configuration files as Nit objects.
+# However, it needs to know which Nit class to recreate from each JSON object.
+# The class is either declared or inferred:
+#
+# 1. The JSON object defines a `__class` key with the name of the Nit class as value.
+#    This attribute is generated by the `JsonSerializer` with other metadata,
+#    it can also be specified by other external tools.
+# 2. A refinement of `JsonDeserializer::class_name_heuristic` identifies the Nit class.
+# 3. If all else fails, `JsonDeserializer` uses the static type of the attribute.
 #
 # ### Usage Example
 #
 # ~~~nitish
 # import json::serialization
 #
-# class MeetupConfig
+# class Triangle
 #     serialize
 #
-#     var description: String
-#     var max_participants: nullable Int
-#     var answers: Array[FlatString]
+#     var corners = new Array[Point]
+#     redef var to_s is serialize_as("name")
 # end
 #
-# var json_code = """
-# {"__class": "MeetupConfig", "description": "My Awesome Meetup", "max_participants": null, "answers": ["Pepperoni", "Chicken"]}"""
-# var deserializer = new JsonDeserializer(json_code)
+# class Point
+#     serialize
+#
+#     var x: Int
+#     var y: Int
+# end
 #
-# var meet = deserializer.deserialize
+# # Metadata on each JSON object tells the deserializer what is its Nit type,
+# # and it supports special types such as generic collections.
+# var json_with_metadata = """{
+#     "__class": "Triangle",
+#     "corners": {"__class": "Array[Point]",
+#                 "__items": [{"__class": "Point", "x": 0, "y": 0},
+#                             {"__class": "Point", "x": 3, "y": 0},
+#                             {"__class": "Point", "x": 2, "y": 2}]},
+#     "name": "some triangle"
+# }"""
 #
-# # Check for errors
+# var deserializer = new JsonDeserializer(json_with_metadata)
+# var object = deserializer.deserialize
 # assert deserializer.errors.is_empty
+# assert object != null
+# print object
+#
+# # However most non-Nit services won't add the metadata and instead produce plain JSON.
+# # Without a "__class", the deserializer relies on `class_name_heuristic` and the static type.
+# # The type of the root object to deserialize can be specified by calling
+# # its deserialization constructor `from_deserializer`.
+# var plain_json = """{
+#     "corners": [{"x": 0, "y": 0},
+#                 {"x": 3, "y": 0},
+#                 {"x": 2, "y": 2}],
+#     "name": "the same triangle"
+# }"""
 #
-# assert meet isa MeetupConfig
-# assert meet.description == "My Awesome Meetup"
-# assert meet.max_participants == null
-# assert meet.answers == ["Pepperoni", "Chicken"]
+# deserializer = new JsonDeserializer(plain_json)
+# object = new Triangle.from_deserializer(deserializer)
+# assert deserializer.errors.is_empty # If false, `obj` is invalid
+# print object
 # ~~~
 module serialization
 
@@ -242,6 +274,9 @@ class JsonDeserializer
        # Depth-first path in the serialized object tree.
        private var path = new Array[Map[String, nullable Object]]
 
+       # Names of the attributes from the root to the object currently being deserialized
+       var attributes_path = new Array[String]
+
        # Last encountered object reference id.
        #
        # See `id_to_object`.
@@ -253,7 +288,7 @@ class JsonDeserializer
                self.root = root
        end
 
-       redef fun deserialize_attribute(name)
+       redef fun deserialize_attribute(name, static_type)
        do
                assert not path.is_empty # This is an internal error, abort
                var current = path.last
@@ -265,7 +300,10 @@ class JsonDeserializer
 
                var value = current[name]
 
-               return convert_object(value)
+               attributes_path.add name
+               var res = convert_object(value, static_type)
+               attributes_path.pop
+               return res
        end
 
        # This may be called multiple times by the same object from constructors
@@ -277,8 +315,8 @@ class JsonDeserializer
                cache[id] = new_object
        end
 
-       # Convert from simple Json object to Nit object
-       private fun convert_object(object: nullable Object): nullable Object
+       # Convert the simple JSON `object` to a Nit object
+       private fun convert_object(object: nullable Object, static_type: nullable String): nullable Object
        do
                if object isa JsonParseError then
                        errors.add object
@@ -333,6 +371,14 @@ class JsonDeserializer
                                if class_name == null then
                                        # Fallback to custom heuristic
                                        class_name = class_name_heuristic(object)
+
+                                       if class_name == null and static_type != null then
+                                               # Fallack to the static type, strip the `nullable` prefix
+                                               var prefix = "nullable "
+                                               if static_type.has(prefix) then
+                                                       class_name = static_type.substring_from(prefix.length)
+                                               else class_name = static_type
+                                       end
                                end
 
                                if class_name == null then
@@ -381,6 +427,23 @@ class JsonDeserializer
 
                # Simple JSON array without serialization metadata
                if object isa Array[nullable Object] then
+                       # Can we use the static type?
+                       if static_type != null then
+                               var prefix = "nullable "
+                               var class_name = if static_type.has(prefix) then
+                                               static_type.substring_from(prefix.length)
+                                       else static_type
+
+                               opened_array = object
+                               var value = deserialize_class(class_name)
+                               opened_array = null
+                               return value
+                       end
+
+                       # This branch should rarely be used:
+                       # when an array is the root object which is accepted but illegal in standard JSON,
+                       # or in strange custom deserialization hacks.
+
                        var array = new Array[nullable Object]
                        var types = new HashSet[String]
                        var has_nullable = false
@@ -428,24 +491,32 @@ class JsonDeserializer
                                return typed_array
                        end
 
-                       # Uninferable type, return as `Array[nullable Object]`
+                       # Uninferrable type, return as `Array[nullable Object]`
                        return array
                end
 
                return object
        end
 
+       # Current array open for deserialization, used by `SimpleCollection::from_deserializer`
+       private var opened_array: nullable Array[nullable Object] = null
+
        redef fun deserialize
        do
                errors.clear
                return convert_object(root)
        end
 
-       # User customizable heuristic to get the name of the Nit class to deserialize `json_object`
+       # User customizable heuristic to infer the name of the Nit class to deserialize `json_object`
        #
        # This method is called only when deserializing an object without the metadata `__class`.
-       # Return the class name as a `String` when it can be inferred.
-       # Return `null` when the class name cannot be found.
+       # Use the content of `json_object` to identify what Nit class it should be deserialized into.
+       # Or use `self.attributes_path` indicating where the deserialized object will be stored,
+       # is is less reliable as some objects don't have an associated attribute:
+       # the root/first deserialized object and collection elements.
+       #
+       # Return the class name as a `String` when it can be inferred,
+       # or `null` when the class name cannot be found.
        #
        # If a valid class name is returned, `json_object` will then be deserialized normally.
        # So it must contain the attributes of the corresponding class, as usual.
@@ -461,6 +532,7 @@ class JsonDeserializer
        #     serialize
        #
        #     var error: String
+       #     var related_data: MyData
        # end
        #
        # class MyJsonDeserializer
@@ -468,18 +540,26 @@ class JsonDeserializer
        #
        #     redef fun class_name_heuristic(json_object)
        #     do
+       #         # Infer the Nit class from the content of the JSON object.
        #         if json_object.keys.has("error") then return "MyError"
        #         if json_object.keys.has("data") then return "MyData"
+       #
+       #         # Infer the Nit class from the attribute where it will be stored.
+       #         # This line duplicates a previous line, and would only apply when
+       #         # `MyData` is within a `MyError`.
+       #         if attributes_path.not_empty and attributes_path.last == "related_data" then return "MyData"
+       #
        #         return null
        #     end
        # end
        #
-       # var json = """{"data": "some other data"}"""
+       # var json = """{"data": "some data"}"""
        # var deserializer = new MyJsonDeserializer(json)
        # var deserialized = deserializer.deserialize
        # assert deserialized isa MyData
        #
-       # json = """{"error": "some error message"}"""
+       # json = """{"error": "some error message",
+       #            "related_data": {"data": "some other data"}"""
        # deserializer = new MyJsonDeserializer(json)
        # deserialized = deserializer.deserialize
        # assert deserialized isa MyError
@@ -656,15 +736,34 @@ redef class SimpleCollection[E]
                        v.notify_of_creation self
                        init
 
-                       var arr = v.path.last.get_or_null("__items")
-                       if not arr isa SequenceRead[nullable Object] then
-                               # If there is nothing, we consider that it is an empty collection.
-                               if arr != null then v.errors.add new Error("Deserialization Error: invalid format in {self.class_name}")
-                               return
+                       var open_array: nullable SequenceRead[nullable Object] = v.opened_array
+                       if open_array == null then
+                               # With metadata
+                               var arr = v.path.last.get_or_null("__items")
+                               if not arr isa SequenceRead[nullable Object] then
+                                       # If there is nothing, we consider that it is an empty collection.
+                                       if arr != null then v.errors.add new Error("Deserialization Error: invalid format in {self.class_name}")
+                                       return
+                               end
+                               open_array = arr
+                       end
+
+                       # Try to get the name of the single parameter type assuming it is E.
+                       # This does not work in non-generic subclasses,
+                       # when the first parameter is not E, or
+                       # when there is more than one parameter. (The last one could be fixed)
+                       var class_name = class_name
+                       var items_type = null
+                       var bracket_index = class_name.index_of('[')
+                       if bracket_index != -1 then
+                               var start = bracket_index + 1
+                               var ending = class_name.last_index_of(']')
+                               items_type = class_name.substring(start, ending-start)
                        end
 
-                       for o in arr do
-                               var obj = v.convert_object(o)
+                       # Fill array
+                       for o in open_array do
+                               var obj = v.convert_object(o, items_type)
                                if obj isa E then
                                        add obj
                                else v.errors.add new AttributeTypeError(self, "items", obj, "E")
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 2a5b43a..40d02ef 100644 (file)
 # Basic usage example:
 # ~~~~
 # class MyAction
-#      super Action
-#
-#      redef fun answer(http_request, turi)
-#      do
-#              var response = new HttpResponse(200)
-#              response.body = """
-#              <!DOCTYPE html>
-#              <head>
-#                      <meta charset="utf-8">
-#                      <title>Hello World</title>
-#              </head>
-#              <body>
-#                      <p>Hello World</p>
-#              </body>
-#              </html>"""
-#              return response
-#      end
+#     super Action
+#
+#     redef fun answer(http_request, turi)
+#     do
+#         var response = new HttpResponse(200)
+#         response.body = """
+# <!DOCTYPE html>
+# <head>
+#     <meta charset="utf-8">
+#     <title>Hello World</title>
+# </head>
+# <body>
+#     <p>Hello World</p>
+# </body>
+# </html>"""
+#         return response
+#     end
 # end
 #
-# var vh = new VirtualHost("localhost:80")
+# # Listen to port 8080 on all interfaces
+# var vh = new VirtualHost("0.0.0.0:8080")
 #
 # # Serve index.html with our custom handler
 # vh.routes.add new Route("/index.html", new MyAction)
index de9ddc0..66334c3 100644 (file)
@@ -201,8 +201,15 @@ redef class Interfaces
        redef fun add(e)
        do
                super
-               var config = vh.server_config
-               if config != null then sys.listen_on(e, config.factory)
+               var config = virtual_host.server_config
+               if config != null then register_and_listen(e, config)
+       end
+
+       # Indirection to `listen_on` and check if this targets all addresses
+       private fun register_and_listen(e: Interface, config: ServerConfig)
+       do
+               listen_on(e, config.factory)
+               if e.name == "0.0.0.0" or e.name == "::0" then config.default_virtual_host = virtual_host
        end
 
        # TODO remove
@@ -212,7 +219,7 @@ redef class VirtualHosts
        redef fun add(e)
        do
                super
-               for i in e.interfaces do sys.listen_on(i, config.factory)
+               for i in e.interfaces do e.interfaces.register_and_listen(i, config)
        end
 
        # TODO remove
index e61caad..3cf51ff 100644 (file)
@@ -25,7 +25,7 @@ class ServerConfig
        var virtual_hosts = new VirtualHosts(self)
 
        # Default `VirtualHost` to respond to requests not handled by any of the `virtual_hosts`
-       var default_virtual_host: nullable VirtualHost = null
+       var default_virtual_host: nullable VirtualHost = null is writable
 end
 
 # A `VirtualHost` configuration
@@ -82,7 +82,7 @@ class Interfaces
        super Array[Interface]
 
        # Back reference to the associtated `VirtualHost`
-       var vh: VirtualHost
+       var virtual_host: VirtualHost
 
        # Add an `Interface` described by `text` formatted as `interface.name.com:port`
        fun add_from_string(text: String)
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`.
index 787f51c..47eef73 100644 (file)
@@ -260,22 +260,12 @@ assert couple == deserialize_couple
 
 The serialization has some limitations:
 
-* Not enough classes from the standard library are supported.
-  This only requires someone to actually code the support.
-  It should not be especially hard for most classes, some can
-  simply declare the `serialize` annotation.
-
-* A limitation of the Json parser prevents deserializing from files
+* A limitation of the JSON parser prevents deserializing from files
   with more than one object.
   This could be improved in the future, but for now you should
-  serialize a single object to each filesand use different instances of
+  serialize a single object to each files and use different instances of
   serializer and deserializer each time.
 
-* The `serialize` annotation does not handle very well
-  complex constructors. This could be improved in the compiler.
-  For now, you may prefer to use `serialize` on simple classes,
-  of by using custom `Serializable`.
-
 * The serialization uses only the short name of a class, not its qualified name.
   This will cause problem when different classes using the same name.
   This could be solved partially in the compiler and the library.
@@ -283,7 +273,7 @@ The serialization has some limitations:
   the different programs sharing the serialized data.
 
 * The serialization support in the compiler need some help to
-  deal with generic types. The solution is to use `nitserial`,
+  deal with generic types. A solution is to use `nitserial`,
   the next section explores this subject.
 
 ## Dealing with generic types
index 0ad3019..7ecc7d2 100644 (file)
@@ -88,21 +88,30 @@ end
 
 # Abstract deserialization service
 #
-# After initialization of one of its sub-classes, call `deserialize`
+# The main service is `deserialize`.
 abstract class Deserializer
-       # Main method of this class, returns a Nit object
+       # Deserialize and return an object, storing errors in the attribute `errors`
+       #
+       # This method behavior varies according to the implementation engines.
        fun deserialize: nullable Object is abstract
 
-       # Internal method to be implemented by sub-classes
-       fun deserialize_attribute(name: String): nullable Object is abstract
+       # Deserialize the attribute with `name` from the object open for deserialization
+       #
+       # The `static_type` can be used as last resort if the deserialized object
+       # desn't have any metadata declaring the dynamic type.
+       #
+       # Internal method to be implemented by the engines.
+       fun deserialize_attribute(name: String, static_type: nullable String): nullable Object is abstract
 
-       # Internal method called by objects in creation,
-       # to be implemented by sub-classes
+       # Register a newly allocated object (even if not completely built)
+       #
+       # Internal method called by objects in creation, to be implemented by the engines.
        fun notify_of_creation(new_object: Object) is abstract
 
        # Deserialize the next available object as an instance of `class_name`
        #
-       # Returns the deserialized object on success, aborts on error.
+       # Return the deserialized object on success and
+       # record in `errors` if `class_name` is unknown.
        #
        # This method should be redefined for each custom subclass of `Serializable`.
        # All refinement should look for a precise `class_name` and call super
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 bb5cb8d..6633e13 100644 (file)
@@ -22,6 +22,7 @@ module serialization_phase
 private import parser_util
 import modelize
 private import annotation
+intrude import literal
 
 redef class ToolContext
 
@@ -34,9 +35,7 @@ redef class ToolContext
 
        # The second phase of the serialization
        var serialization_phase_post_model: Phase = new SerializationPhasePostModel(self,
-               [modelize_class_phase, serialization_phase_pre_model])
-
-       private fun place_holder_type_name: String do return "PlaceHolderTypeWhichShouldNotExist"
+               [modelize_property_phase, serialization_phase_pre_model])
 end
 
 redef class ANode
@@ -45,8 +44,6 @@ redef class ANode
 
        # Is this node annotated to not be made serializable?
        private fun is_noserialize: Bool do return false
-
-       private fun accept_precise_type_visitor(v: PreciseTypeVisitor) do visit_all(v)
 end
 
 redef class ADefinition
@@ -121,7 +118,7 @@ private class SerializationPhasePreModel
                if not node isa AModuledecl then
                        var up_serialize = false
                        var up: nullable ANode = node
-                       loop
+                       while up != null do
                                up = up.parent
                                if up == null then
                                        break
@@ -160,7 +157,7 @@ private class SerializationPhasePreModel
                        # Add services
                        var per_attribute = not serialize_by_default
                        generate_serialization_method(nclassdef, per_attribute)
-                       generate_deserialization_init(nclassdef, per_attribute)
+                       generate_deserialization_init(nclassdef)
                end
        end
 
@@ -170,18 +167,16 @@ private class SerializationPhasePreModel
                nmodule.inits_to_retype.clear
 
                # collect all classes
-               var auto_serializable_nclassdefs = new Array[AStdClassdef]
-               for nclassdef in nmodule.n_classdefs do
-                       if nclassdef isa AStdClassdef and nclassdef.how_serialize != null then
-                               auto_serializable_nclassdefs.add nclassdef
-                       end
-               end
-
+               var auto_serializable_nclassdefs = nmodule.auto_serializable_nclassdefs
                if not auto_serializable_nclassdefs.is_empty then
                        generate_deserialization_method(nmodule, auto_serializable_nclassdefs)
                end
        end
 
+       # Implement `core_serialize_to` on `nclassdef`
+       #
+       # Are attributes serialized on demand `per_attribute` with `serialize`?
+       # Otherwise they are serialized by default, and we check instead for `noserialize`.
        fun generate_serialization_method(nclassdef: AClassdef, per_attribute: Bool)
        do
                var npropdefs = nclassdef.n_propdefs
@@ -206,8 +201,10 @@ private class SerializationPhasePreModel
                npropdefs.push(toolcontext.parse_propdef(code.join("\n")))
        end
 
-       # Add a constructor to the automated nclassdef
-       fun generate_deserialization_init(nclassdef: AClassdef, per_attribute: Bool)
+       # Add an empty constructor to the automated nclassdef
+       #
+       # Will be filled by `SerializationPhasePostModel`.
+       fun generate_deserialization_init(nclassdef: AClassdef)
        do
                var npropdefs = nclassdef.n_propdefs
 
@@ -221,6 +218,75 @@ private class SerializationPhasePreModel
                        end
                end
 
+               var code = """
+redef init from_deserializer(v: Deserializer) do abort"""
+
+               var npropdef = toolcontext.parse_propdef(code).as(AMethPropdef)
+               npropdefs.add npropdef
+               nclassdef.parent.as(AModule).inits_to_retype.add npropdef
+       end
+
+       # Add an empty `Deserializer::deserialize_class_intern`
+       #
+       # Will be filled by `SerializationPhasePostModel`.
+       fun generate_deserialization_method(nmodule: AModule, nclassdefs: Array[AStdClassdef])
+       do
+               var code = new Array[String]
+
+               var deserializer_nclassdef = nmodule.deserializer_nclassdef
+               var deserializer_npropdef
+               if deserializer_nclassdef == null then
+                       # create the class
+                       code.add "redef class Deserializer"
+                       deserializer_npropdef = null
+               else
+                       deserializer_npropdef = deserializer_nclassdef.deserializer_npropdef
+               end
+
+               if deserializer_npropdef == null then
+                       # create the property
+                       code.add "      redef fun deserialize_class_intern(name) do abort"
+               else
+                       toolcontext.error(deserializer_npropdef.location, "Error: `Deserializer::deserialize_class_intern` is generated and must not be defined, use `deserialize_class` instead.")
+                       return
+               end
+
+               if deserializer_nclassdef == null then
+                       code.add "end"
+                       nmodule.n_classdefs.add toolcontext.parse_classdef(code.join("\n"))
+               else
+                       deserializer_nclassdef.n_propdefs.add(toolcontext.parse_propdef(code.join("\n")))
+               end
+       end
+end
+
+private class SerializationPhasePostModel
+       super Phase
+
+       # Fill the deserialization init `from_deserializer` and `Deserializer.deserialize_class_intern`
+       redef fun process_nmodule(nmodule)
+       do
+               for npropdef in nmodule.inits_to_retype do
+                       var nclassdef = npropdef.parent
+                       assert nclassdef isa AStdClassdef
+
+                       var serialize_by_default = nclassdef.how_serialize
+                       assert serialize_by_default != null
+
+                       var per_attribute = not serialize_by_default
+                       fill_deserialization_init(nclassdef, npropdef, per_attribute)
+               end
+
+               # collect all classes
+               var auto_serializable_nclassdefs = nmodule.auto_serializable_nclassdefs
+               if not auto_serializable_nclassdefs.is_empty then
+                       fill_deserialization_method(nmodule, auto_serializable_nclassdefs)
+               end
+       end
+
+       # Fill the constructor to the generated `init_npropdef` of `nclassdef`
+       fun fill_deserialization_init(nclassdef: AClassdef, init_npropdef: AMethPropdef, per_attribute: Bool)
+       do
                var code = new Array[String]
                code.add """
 redef init from_deserializer(v: Deserializer)
@@ -229,35 +295,28 @@ do
        v.notify_of_creation self
 """
 
-               for attribute in npropdefs do if attribute isa AAttrPropdef then
+               for attribute in nclassdef.n_propdefs do
+                       if not attribute isa AAttrPropdef then continue
 
                        # Is `attribute` to be skipped?
                        if (per_attribute and not attribute.is_serialize) or
                                attribute.is_noserialize then continue
 
-                       var n_type = attribute.n_type
-                       var type_name
-                       var type_name_pretty
-                       if n_type == null then
-                               # Use a place holder, we will replace it with the inferred type after the model phases
-                               type_name = toolcontext.place_holder_type_name
-                               type_name_pretty = "Unknown type"
-                       else
-                               type_name = n_type.type_name
-                               type_name_pretty = type_name
-                       end
+                       var mtype = attribute.mtype
+                       if mtype == null then continue
+                       var type_name = mtype.to_s
                        var name = attribute.name
 
                        if type_name == "nullable Object" then
                                # Don't type check
                                code.add """
-       var {{{name}}} = v.deserialize_attribute("{{{attribute.serialize_name}}}")
+       var {{{name}}} = v.deserialize_attribute("{{{attribute.serialize_name}}}", "{{{type_name}}}")
 """
                        else code.add """
-       var {{{name}}} = v.deserialize_attribute("{{{attribute.serialize_name}}}")
+       var {{{name}}} = v.deserialize_attribute("{{{attribute.serialize_name}}}", "{{{type_name}}}")
        if not {{{name}}} isa {{{type_name}}} then
                # Check if it was a subjectent error
-               v.errors.add new AttributeTypeError(self, "{{{attribute.serialize_name}}}", {{{name}}}, "{{{type_name_pretty}}}")
+               v.errors.add new AttributeTypeError(self, "{{{attribute.serialize_name}}}", {{{name}}}, "{{{type_name}}}")
 
                # Clear subjacent error
                if v.keep_going == false then return
@@ -269,94 +328,93 @@ do
 
                code.add "end"
 
+               # Replace the body of the constructor
                var npropdef = toolcontext.parse_propdef(code.join("\n")).as(AMethPropdef)
-               npropdefs.add npropdef
-               nclassdef.parent.as(AModule).inits_to_retype.add npropdef
+               init_npropdef.n_block = npropdef.n_block
+
+               # Run the literal phase on the generated code
+               var v = new LiteralVisitor(toolcontext)
+               v.enter_visit(npropdef.n_block)
        end
 
-       # Added to the abstract serialization service
-       fun generate_deserialization_method(nmodule: AModule, nclassdefs: Array[AStdClassdef])
+       # Fill the abstract serialization service
+       fun fill_deserialization_method(nmodule: AModule, nclassdefs: Array[AStdClassdef])
        do
-               var code = new Array[String]
-
                var deserializer_nclassdef = nmodule.deserializer_nclassdef
-               var deserializer_npropdef
-               if deserializer_nclassdef == null then
-                       # create the class
-                       code.add "redef class Deserializer"
-                       deserializer_npropdef = null
-               else
-                       deserializer_npropdef = deserializer_nclassdef.deserializer_npropdef
-               end
+               if deserializer_nclassdef == null then return
+               var deserializer_npropdef = deserializer_nclassdef.deserializer_npropdef
+               if deserializer_npropdef == null then return
 
-               if deserializer_npropdef == null then
-                       # create the property
-                       code.add "      redef fun deserialize_class_intern(name)"
-                       code.add "      do"
-               else
-                       toolcontext.error(deserializer_npropdef.location, "Error: `Deserializer::deserialize_class_intern` is generated and must not be defined, use `deserialize_class` instead.")
-                       return
-               end
+               # Collect local types expected to be deserialized
+               var types_to_deserialize = new Set[String]
 
+               ## Local serializable standard class without parameters
                for nclassdef in nclassdefs do
-                       var name = nclassdef.n_qid.n_id.text
-                       if nclassdef.n_formaldefs.is_empty and
-                          nclassdef.n_classkind isa AConcreteClasskind then
+                       var mclass = nclassdef.mclass
+                       if mclass == null then continue
 
-                               code.add "              if name == \"{name}\" then return new {name}.from_deserializer(self)"
+                       if mclass.arity == 0 and mclass.kind == concrete_kind then
+                               types_to_deserialize.add mclass.name
                        end
                end
 
-               code.add "              return super"
-               code.add "      end"
+               ## Static parametized types on serializable attributes
+               for nclassdef in nmodule.n_classdefs do
+                       if not nclassdef isa AStdClassdef then continue
 
-               if deserializer_nclassdef == null then
-                       code.add "end"
-                       nmodule.n_classdefs.add toolcontext.parse_classdef(code.join("\n"))
-               else
-                       deserializer_nclassdef.n_propdefs.add(toolcontext.parse_propdef(code.join("\n")))
-               end
-       end
-end
+                       for attribute in nclassdef.n_propdefs do
+                               if not attribute isa AAttrPropdef then continue
 
-private class SerializationPhasePostModel
-       super Phase
+                               var serialize_by_default = nclassdef.how_serialize
+                               if serialize_by_default == null then continue
+                               var per_attribute = not serialize_by_default
 
-       redef fun process_nmodule(nmodule)
-       do
-               for npropdef in nmodule.inits_to_retype do
-                       var mpropdef = npropdef.mpropdef
-                       if mpropdef == null then continue # skip error
-                       var v = new PreciseTypeVisitor(npropdef, mpropdef.mclassdef, toolcontext)
-                       npropdef.accept_precise_type_visitor v
-               end
-       end
-end
+                               # Is `attribute` to be skipped?
+                               if (per_attribute and not attribute.is_serialize) or
+                                       attribute.is_noserialize then continue
 
-# Visitor on generated constructors to replace the expected type of deserialized attributes
-private class PreciseTypeVisitor
-       super Visitor
+                               var mtype = attribute.mtype
+                               if mtype == null then continue
+                               if mtype isa MNullableType then mtype = mtype.mtype
 
-       var npropdef: AMethPropdef
-       var mclassdef: MClassDef
-       var toolcontext: ToolContext
+                               if mtype isa MClassType and mtype.mclass.arity > 0 and
+                                  mtype.mclass.kind == concrete_kind and not mtype.need_anchor then
 
-       redef fun visit(n) do n.accept_precise_type_visitor(self)
-end
+                                       # Check is a `Serializable`
+                                       var mmodule = nmodule.mmodule
+                                       if mmodule == null then continue
 
-redef class AIsaExpr
-       redef fun accept_precise_type_visitor(v)
-       do
-               if n_type.collect_text != v.toolcontext.place_holder_type_name then return
-
-               var attr_name = "_" + n_expr.collect_text
-               for mattrdef in v.mclassdef.mpropdefs do
-                       if mattrdef isa MAttributeDef and mattrdef.name == attr_name then
-                               var new_ntype = v.toolcontext.parse_something(mattrdef.static_mtype.to_s)
-                               n_type.replace_with new_ntype
-                               break
+                                       var greaters = mtype.mclass.in_hierarchy(mmodule).greaters
+                                       var is_serializable = false
+                                       for sup in greaters do if sup.name == "Serializable" then
+                                               is_serializable = true
+                                               break
+                                       end
+
+                                       if is_serializable then types_to_deserialize.add mtype.to_s
+                               end
                        end
                end
+
+               # Build implementation code
+               var code = new Array[String]
+               code.add "redef fun deserialize_class_intern(name)"
+               code.add "do"
+
+               for name in types_to_deserialize do
+                       code.add "      if name == \"{name}\" then return new {name}.from_deserializer(self)"
+               end
+
+               code.add "      return super"
+               code.add "end"
+
+               # Replace the body of the constructor
+               var npropdef = toolcontext.parse_propdef(code.join("\n")).as(AMethPropdef)
+               deserializer_npropdef.n_block = npropdef.n_block
+
+               # Run the literal phase on the generated code
+               var v = new LiteralVisitor(toolcontext)
+               v.enter_visit(npropdef.n_block)
        end
 end
 
@@ -387,9 +445,9 @@ redef class AModule
        private fun deserializer_nclassdef: nullable AStdClassdef
        do
                for nclassdef in n_classdefs do
-                       if nclassdef isa AStdClassdef and nclassdef.n_qid.n_id.text == "Deserializer" then
-                               return nclassdef
-                       end
+                       if not nclassdef isa AStdClassdef then continue
+                       var n_qid = nclassdef.n_qid
+                       if n_qid != null and n_qid.n_id.text == "Deserializer" then return nclassdef
                end
 
                return null
@@ -397,7 +455,22 @@ redef class AModule
 
        private var inits_to_retype = new Array[AMethPropdef]
 
-       redef fun is_serialize do return n_moduledecl != null and n_moduledecl.is_serialize
+       redef fun is_serialize
+       do
+               var n_moduledecl = n_moduledecl
+               return n_moduledecl != null and n_moduledecl.is_serialize
+       end
+
+       # `AStdClassdef` marked as serializable, itself or one of theur attribute
+       private var auto_serializable_nclassdefs: Array[AStdClassdef] is lazy do
+               var array = new Array[AStdClassdef]
+               for nclassdef in n_classdefs do
+                       if nclassdef isa AStdClassdef and nclassdef.how_serialize != null then
+                               array.add nclassdef
+                       end
+               end
+               return array
+       end
 end
 
 redef class AStdClassdef
index 7c6cdc1..9484425 100644 (file)
 # Runs a webserver based on nitcorn that render things from model.
 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
 
-       # Host name to bind on.
+       # Path to app config file.
+       var opt_config = new OptionString("Path to app config file", "--config")
+
+       # Host name to bind on (will overwrite the config one).
        var opt_host = new OptionString("Host to bind the server on", "--host")
 
-       # Port number to bind on.
-       var opt_port = new OptionInt("Port number to use", 3000, "--port")
+       # Port number to bind on (will overwrite the config one).
+       var opt_port = new OptionInt("Port number to use", -1, "--port")
 
        # Web rendering phase.
        var webphase: Phase = new NitwebPhase(self, null)
 
        init do
                super
-               option_context.add_option(opt_host, opt_port)
+               option_context.add_option(opt_config, opt_host, opt_port)
        end
 end
 
 # Phase that builds the model and wait for http request to serve pages.
 private class NitwebPhase
        super Phase
-       redef fun process_mainmodule(mainmodule, mmodules)
-       do
-               var model = mainmodule.model
-               var modelbuilder = toolcontext.modelbuilder
 
-               # Build catalog
+       # Build the nitweb config from `toolcontext` options.
+       fun build_config(toolcontext: ToolContext, mainmodule: MModule): NitwebConfig do
+               var config_file = toolcontext.opt_config.value
+               if config_file == null then config_file = "nitweb.ini"
+               var config = new NitwebConfig(
+                       config_file,
+                       toolcontext.modelbuilder.model,
+                       mainmodule,
+                       toolcontext.modelbuilder)
+               var opt_host = toolcontext.opt_host.value
+               if opt_host != null then config["app.host"] = opt_host
+               var opt_port = toolcontext.opt_port.value
+               if opt_port >= 0 then config["app.port"] = opt_port.to_s
+               return config
+       end
+
+       # Build the nit catalog used in homepage.
+       fun build_catalog(model: Model, modelbuilder: ModelBuilder): Catalog do
                var catalog = new Catalog(modelbuilder)
                for mpackage in model.mpackages do
                        catalog.deps.add_node(mpackage)
@@ -59,61 +94,54 @@ private class NitwebPhase
                        catalog.git_info(mpackage)
                        catalog.package_page(mpackage)
                end
+               return catalog
+       end
 
-               # Prepare mongo connection
-               var mongo = new MongoClient("mongodb://localhost:27017/")
-               var db = mongo.database("nitweb")
-               var collection = db.collection("stars")
-
-               # Run the server
-               var host = toolcontext.opt_host.value or else "localhost"
-               var port = toolcontext.opt_port.value
+       redef fun process_mainmodule(mainmodule, mmodules)
+       do
+               var model = mainmodule.model
+               var modelbuilder = toolcontext.modelbuilder
+               var config = build_config(toolcontext, mainmodule)
+               var catalog = build_catalog(model, modelbuilder)
 
                var app = new App
 
+               app.use_before("/*", new SessionInit)
                app.use_before("/*", new RequestClock)
-               app.use("/api", new APIRouter(model, modelbuilder, mainmodule, catalog, collection))
+               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)
 
-               app.listen(host, port.to_i)
+               app.listen(config.app_host, config.app_port)
        end
 end
 
 # Group all api handlers in one router.
-class APIRouter
-       super Router
-
-       # Model to pass to handlers.
-       var model: Model
-
-       # ModelBuilder to pass to handlers.
-       var modelbuilder: ModelBuilder
-
-       # Mainmodule to pass to handlers.
-       var mainmodule: MModule
+class NitwebAPIRouter
+       super APIRouter
 
        # Catalog to pass to handlers.
        var catalog: Catalog
 
-       # Mongo collection used to store ratings.
-       var collection: MongoCollection
-
        init do
-               use("/catalog", new APICatalogRouter(model, mainmodule, catalog))
-               use("/list", new APIList(model, mainmodule))
-               use("/search", new APISearch(model, mainmodule))
-               use("/random", new APIRandom(model, mainmodule))
-               use("/entity/:id", new APIEntity(model, mainmodule))
-               use("/code/:id", new APIEntityCode(model, mainmodule, modelbuilder))
-               use("/uml/:id", new APIEntityUML(model, mainmodule))
-               use("/linearization/:id", new APIEntityLinearization(model, mainmodule))
-               use("/defs/:id", new APIEntityDefs(model, mainmodule))
-               use("/feedback/", new APIFeedbackRouter(model, mainmodule, collection))
-               use("/inheritance/:id", new APIEntityInheritance(model, mainmodule))
-               use("/graph/", new APIGraphRouter(model, mainmodule))
-               use("/docdown/", new APIDocdown(model, mainmodule, modelbuilder))
-               use("/metrics/", new APIMetricsRouter(model, mainmodule))
+               use("/catalog", new APICatalogRouter(config, catalog))
+               use("/list", new APIList(config))
+               use("/search", new APISearch(config))
+               use("/random", new APIRandom(config))
+               use("/entity/:id", new APIEntity(config))
+               use("/code/:id", new APIEntityCode(config))
+               use("/uml/:id", new APIEntityUML(config))
+               use("/linearization/:id", new APIEntityLinearization(config))
+               use("/defs/:id", new APIEntityDefs(config))
+               use("/feedback/", new APIFeedbackRouter(config))
+               use("/inheritance/:id", new APIEntityInheritance(config))
+               use("/graph/", new APIGraphRouter(config))
+               use("/docdown/", new APIDocdown(config))
+               use("/metrics/", new APIMetricsRouter(config))
+               use("/user", new GithubUser)
        end
 end
 
@@ -121,7 +149,7 @@ end
 var toolcontext = new ToolContext
 var tpl = new Template
 tpl.add "Usage: nitweb [OPTION]... <file.nit>...\n"
-tpl.add "Run a webserver based on nitcorn that serve pages about model."
+tpl.add "Run a webserver based on nitcorn that serves pages about model."
 toolcontext.tooldescription = tpl.write_to_string
 
 # process options
index 72d207f..f0c70c6 100644 (file)
@@ -19,23 +19,17 @@ import catalog
 
 # Group all api handlers in one router.
 class APICatalogRouter
-       super Router
-
-       # Model to pass to handlers.
-       var model: Model
-
-       # Mainmodule to pass to handlers.
-       var mainmodule: MModule
+       super APIRouter
 
        # Catalog to pass to handlers.
        var catalog: Catalog
 
        init do
-               use("/highlighted", new APICatalogHighLighted(model, mainmodule, catalog))
-               use("/required", new APICatalogMostRequired(model, mainmodule, catalog))
-               use("/bytags", new APICatalogByTags(model, mainmodule, catalog))
-               use("/contributors", new APICatalogContributors(model, mainmodule, catalog))
-               use("/stats", new APICatalogStats(model, mainmodule, catalog))
+               use("/highlighted", new APICatalogHighLighted(config, catalog))
+               use("/required", new APICatalogMostRequired(config, catalog))
+               use("/bytags", new APICatalogByTags(config, catalog))
+               use("/contributors", new APICatalogContributors(config, catalog))
+               use("/stats", new APICatalogStats(config, catalog))
        end
 end
 
@@ -74,7 +68,7 @@ class APICatalogStats
 
        redef fun get(req, res) do
                var obj = new JsonObject
-               obj["packages"] = model.mpackages.length
+               obj["packages"] = config.model.mpackages.length
                obj["maintainers"] = catalog.maint2proj.length
                obj["contributors"] = catalog.contrib2proj.length
                obj["modules"] = catalog.mmodules.sum
@@ -97,7 +91,7 @@ class APICatalogMostRequired
        redef fun get(req, res) do
                if catalog.deps.not_empty then
                        var reqs = new Counter[MPackage]
-                       for p in model.mpackages do
+                       for p in config.model.mpackages do
                                reqs[p] = catalog.deps[p].smallers.length - 1
                        end
                        res.json list_best(reqs)
index 3510b64..bf4c80f 100644 (file)
@@ -24,13 +24,10 @@ import doc_commands
 class APIDocdown
        super APIHandler
 
-       # Modelbuilder used by the commands
-       var modelbuilder: ModelBuilder
-
        # Specific Markdown processor to use within Nitweb
        var md_processor: MarkdownProcessor is lazy do
                var proc = new MarkdownProcessor
-               proc.emitter.decorator = new NitwebDecorator(view, modelbuilder)
+               proc.emitter.decorator = new NitwebDecorator(view, config.modelbuilder)
                return proc
        end
 
index 61ba756..76e9d67 100644 (file)
@@ -18,21 +18,38 @@ module api_feedback
 import web_base
 import mongodb
 
-# Group all api handlers in one router
-class APIFeedbackRouter
-       super Router
+redef class NitwebConfig
+
+       # MongoDB uri used for data persistence.
+       #
+       # * key: `mongo.uri`
+       # * default: `mongodb://localhost:27017/`
+       var mongo_uri: String is lazy do
+               return value_or_default("mongo.uri", "mongodb://localhost:27017/")
+       end
 
-       # Model to pass to handlers
-       var model: Model
+       # MongoDB DB used for data persistence.
+       #
+       # * key: `mongo.db`
+       # * default: `nitweb`
+       var mongo_db: String is lazy do return value_or_default("mongo.db", "nitweb")
 
-       # Mainmodule to pass to handlers
-       var mainmodule: MModule
+       # Mongo instance
+       var mongo: MongoClient is lazy do return new MongoClient(mongo_uri)
 
-       # Mongo collection used to store ratings
-       var collection: MongoCollection
+       # Database instance
+       var db: MongoDb is lazy do return mongo.database(mongo_db)
+
+       # MongoDB collection used to store stars.
+       var stars: MongoCollection is lazy do return db.collection("stars")
+end
+
+# Group all api handlers in one router
+class APIFeedbackRouter
+       super APIRouter
 
        init do
-               use("/stars/:id", new APIStars(model, mainmodule, collection))
+               use("/stars/:id", new APIStars(config))
        end
 end
 
@@ -40,9 +57,6 @@ end
 class APIStars
        super APIHandler
 
-       # Collection used to store ratings
-       var collection: MongoCollection
-
        redef fun get(req, res) do
                var mentity = mentity_from_uri(req, res)
                if mentity == null then
@@ -71,7 +85,7 @@ class APIStars
                end
 
                var val = new MEntityRating(mentity.full_name, rating, get_time)
-               collection.insert(val.json)
+               config.stars.insert(val.json)
 
                res.json mentity_ratings(mentity)
        end
@@ -82,7 +96,7 @@ class APIStars
 
                var req = new JsonObject
                req["mentity"] = mentity.full_name
-               var rs = collection.find_all(req)
+               var rs = config.stars.find_all(req)
                for r in rs do ratings.ratings.add new MEntityRating.from_json(r)
                return ratings
        end
index 67a32e2..d58031d 100644 (file)
@@ -21,16 +21,10 @@ import uml
 
 # Group all api handlers in one router.
 class APIGraphRouter
-       super Router
-
-       # Model to pass to handlers.
-       var model: Model
-
-       # Mainmodule to pass to handlers.
-       var mainmodule: MModule
+       super APIRouter
 
        init do
-               use("/inheritance/:id", new APIInheritanceGraph(model, mainmodule))
+               use("/inheritance/:id", new APIInheritanceGraph(config))
        end
 end
 
index 30a53f1..8d64c04 100644 (file)
@@ -19,16 +19,10 @@ import metrics
 
 # Group all api handlers in one router.
 class APIMetricsRouter
-       super Router
-
-       # Model to pass to handlers.
-       var model: Model
-
-       # Mainmodule to pass to handlers.
-       var mainmodule: MModule
+       super APIRouter
 
        init do
-               use("/structural/:id", new APIStructuralMetrics(model, mainmodule))
+               use("/structural/:id", new APIStructuralMetrics(config))
        end
 end
 
@@ -36,6 +30,7 @@ class APIStructuralMetrics
        super APIHandler
 
        private fun mclasses_metrics: MetricSet do
+               var mainmodule = config.mainmodule
                var metrics = new MetricSet
                metrics.register(new CNOA(mainmodule, view))
                metrics.register(new CNOP(mainmodule, view))
@@ -58,6 +53,7 @@ class APIStructuralMetrics
        end
 
        private fun mmodules_metrics: MetricSet do
+               var mainmodule = config.mainmodule
                var metrics = new MetricSet
                metrics.register(new MNOA(mainmodule, view))
                metrics.register(new MNOP(mainmodule, view))
index f8c6eeb..63fd636 100644 (file)
@@ -148,7 +148,7 @@ class APIEntityLinearization
                        res.error 404
                        return
                end
-               var lin = mentity.collect_linearization(mainmodule)
+               var lin = mentity.collect_linearization(config.mainmodule)
                if lin == null then
                        res.error 404
                        return
@@ -206,7 +206,7 @@ class APIEntityUML
                var dot
                if mentity isa MClassDef then mentity = mentity.mclass
                if mentity isa MClass then
-                       var uml = new UMLModel(view, mainmodule)
+                       var uml = new UMLModel(view, config.mainmodule)
                        dot = uml.generate_class_uml.write_to_string
                else if mentity isa MModule then
                        var uml = new UMLModel(view, mentity)
@@ -225,9 +225,6 @@ end
 class APIEntityCode
        super APIHandler
 
-       # Modelbuilder used to access sources.
-       var modelbuilder: ModelBuilder
-
        redef fun get(req, res) do
                var mentity = mentity_from_uri(req, res)
                if mentity == null then return
@@ -241,7 +238,7 @@ class APIEntityCode
 
        # Highlight `mentity` source code.
        private fun render_source(mentity: MEntity): nullable HTMLTag do
-               var node = modelbuilder.mentity2node(mentity)
+               var node = config.modelbuilder.mentity2node(mentity)
                if node == null then return null
                var hl = new HighlightVisitor
                hl.enter_visit node
index 20d0b05..c535936 100644 (file)
@@ -19,10 +19,11 @@ import model::model_views
 import model::model_json
 import doc_down
 import popcorn
+import popcorn::pop_config
 
-# Specific nitcorn Action that uses a Model
-class ModelHandler
-       super Handler
+# Nitweb config file.
+class NitwebConfig
+       super AppConfig
 
        # Model to use.
        var model: Model
@@ -30,6 +31,17 @@ class ModelHandler
        # MModule used to flatten model.
        var mainmodule: MModule
 
+       # Modelbuilder used to access sources.
+       var modelbuilder: ModelBuilder
+end
+
+# Specific nitcorn Action that uses a Model
+class ModelHandler
+       super Handler
+
+       # App config.
+       var config: NitwebConfig
+
        # Find the MEntity ` with `full_name`.
        fun find_mentity(model: ModelView, full_name: nullable String): nullable MEntity do
                if full_name == null then return null
@@ -38,7 +50,7 @@ class ModelHandler
 
        # Init the model view from the `req` uri parameters.
        fun init_model_view(req: HttpRequest): ModelView do
-               var view = new ModelView(model)
+               var view = new ModelView(config.model)
                var show_private = req.bool_arg("private") or else false
                if not show_private then view.min_visibility = protected_visibility
 
@@ -59,7 +71,7 @@ abstract class APIHandler
        #
        # So we can cache the model view.
        var view: ModelView is lazy do
-               var view = new ModelView(model)
+               var view = new ModelView(config.model)
                view.min_visibility = private_visibility
                view.include_fictive = true
                view.include_empty_doc = true
@@ -87,6 +99,14 @@ abstract class APIHandler
        end
 end
 
+# A Rooter dedicated to APIHandlers.
+class APIRouter
+       super Router
+
+       # App config.
+       var config: NitwebConfig
+end
+
 redef class MEntity
 
        # URL to `self` within the web interface.
index 730ae5c..f2e2aae 100644 (file)
@@ -20,7 +20,7 @@
 <D: <B: <A: false b 123.123 2345 new line ->
 <- false p4ssw0rd> 1111        f"\r\/> true>
 
-Deserialization Error: Doesn't know how to deserialize class "Array", Deserialization Error: Wrong type on `E::a` expected `Unknown type`, got `null`, Deserialization Error: Doesn't know how to deserialize class "Array", Deserialization Error: Wrong type on `E::b` expected `Unknown type`, got `null`
+Deserialization Error: Doesn't know how to deserialize class "Array", Deserialization Error: Wrong type on `E::a` expected `Array[Object]`, got `null`, Deserialization Error: Doesn't know how to deserialize class "Array", Deserialization Error: Wrong type on `E::b` expected `Array[nullable Serializable]`, got `null`
 # Src:
 <E: a: hello, 1234, 123.4; b: hella, 2345, 234.5>
 # Dst:
@@ -28,7 +28,7 @@ Deserialization Error: Doesn't know how to deserialize class "Array", Deserializ
 
 Deserialization Error: Doesn't know how to deserialize class "F"
 Deserialization Error: Doesn't know how to deserialize class "F"
-Deserialization Error: Doesn't know how to deserialize class "HashSet", Deserialization Error: Wrong type on `G::hs` expected `Unknown type`, got `null`, Deserialization Error: Doesn't know how to deserialize class "ArraySet", Deserialization Error: Wrong type on `G::s` expected `Set[String]`, got `null`, Deserialization Error: Doesn't know how to deserialize class "HashMap", Deserialization Error: Wrong type on `G::hm` expected `Unknown type`, got `null`, Deserialization Error: Doesn't know how to deserialize class "ArrayMap", Deserialization Error: Wrong type on `G::am` expected `Unknown type`, got `null`
+Deserialization Error: Doesn't know how to deserialize class "HashSet", Deserialization Error: Wrong type on `G::hs` expected `HashSet[Int]`, got `null`, Deserialization Error: Doesn't know how to deserialize class "ArraySet", Deserialization Error: Wrong type on `G::s` expected `Set[String]`, got `null`, Deserialization Error: Doesn't know how to deserialize class "HashMap", Deserialization Error: Wrong type on `G::hm` expected `HashMap[String, Int]`, got `null`, Deserialization Error: Doesn't know how to deserialize class "ArrayMap", Deserialization Error: Wrong type on `G::am` expected `ArrayMap[String, String]`, got `null`
 # Src:
 <G: hs: -1, 0; s: one, two; hm: one. 1, two. 2; am: three. 3, four. 4>
 # Dst:
index 9c63bff..cb74989 100644 (file)
@@ -46,9 +46,9 @@
 <E: a: hello, 1234, 123.4; b: hella, 2345, 234.5>
 
 Deserialization Error: Doesn't know how to deserialize class "Array"
-Deserialization Error: Wrong type on `E::a` expected `Unknown type`, got `null`
+Deserialization Error: Wrong type on `E::a` expected `Array[Object]`, got `null`
 Deserialization Error: Doesn't know how to deserialize class "Array"
-Deserialization Error: Wrong type on `E::b` expected `Unknown type`, got `null`
+Deserialization Error: Wrong type on `E::b` expected `Array[nullable Serializable]`, got `null`
 # Nit:
 <E: 2222>
 
@@ -79,10 +79,10 @@ Deserialization Error: Doesn't know how to deserialize class "F"
 <G: hs: ; s: ; hm: ; am: >
 
 Deserialization Error: Doesn't know how to deserialize class "HashSet"
-Deserialization Error: Wrong type on `G::hs` expected `Unknown type`, got `null`
+Deserialization Error: Wrong type on `G::hs` expected `HashSet[Int]`, got `null`
 Deserialization Error: Doesn't know how to deserialize class "ArraySet"
 Deserialization Error: Wrong type on `G::s` expected `Set[String]`, got `null`
 Deserialization Error: Doesn't know how to deserialize class "HashMap"
-Deserialization Error: Wrong type on `G::hm` expected `Unknown type`, got `null`
+Deserialization Error: Wrong type on `G::hm` expected `HashMap[String, Int]`, got `null`
 Deserialization Error: Doesn't know how to deserialize class "ArrayMap"
-Deserialization Error: Wrong type on `G::am` expected `Unknown type`, got `null`
+Deserialization Error: Wrong type on `G::am` expected `ArrayMap[String, String]`, got `null`
index fe41ba6..3a3aec0 100644 (file)
 <E: a: hello, 1234, 123.4; b: hella, 2345, 234.5>
 
 Deserialization Error: Doesn't know how to deserialize class "Array"
-Deserialization Error: Wrong type on `E::a` expected `Unknown type`, got `null`
+Deserialization Error: Wrong type on `E::a` expected `Array[Object]`, got `null`
 Deserialization Error: Doesn't know how to deserialize class "Array"
-Deserialization Error: Wrong type on `E::b` expected `Unknown type`, got `null`
+Deserialization Error: Wrong type on `E::b` expected `Array[nullable Serializable]`, got `null`
 # Nit:
 <E: 2222>
 
@@ -197,10 +197,10 @@ Deserialization Error: Doesn't know how to deserialize class "F"
 <G: hs: ; s: ; hm: ; am: >
 
 Deserialization Error: Doesn't know how to deserialize class "HashSet"
-Deserialization Error: Wrong type on `G::hs` expected `Unknown type`, got `null`
+Deserialization Error: Wrong type on `G::hs` expected `HashSet[Int]`, got `null`
 Deserialization Error: Doesn't know how to deserialize class "ArraySet"
 Deserialization Error: Wrong type on `G::s` expected `Set[String]`, got `null`
 Deserialization Error: Doesn't know how to deserialize class "HashMap"
-Deserialization Error: Wrong type on `G::hm` expected `Unknown type`, got `null`
+Deserialization Error: Wrong type on `G::hm` expected `HashMap[String, Int]`, got `null`
 Deserialization Error: Doesn't know how to deserialize class "ArrayMap"
-Deserialization Error: Wrong type on `G::am` expected `Unknown type`, got `null`
+Deserialization Error: Wrong type on `G::am` expected `ArrayMap[String, String]`, got `null`
index 7c0cf6d..a6f1e98 100644 (file)
@@ -1,3 +1,3 @@
 Usage: nitweb [OPTION]... <file.nit>...
-Run a webserver based on nitcorn that serve pages about model.
+Run a webserver based on nitcorn that serves pages about model.
 Use --help for help
diff --git a/wallet b/wallet
new file mode 100755 (executable)
index 0000000..567234d
Binary files /dev/null and b/wallet differ