From: Jean Privat Date: Tue, 10 Mar 2015 13:51:12 +0000 (+0700) Subject: Merge: Abstract attribute X-Git-Tag: v0.7.3~33 X-Git-Url: http://nitlanguage.org?hp=c302ce2ed861df41f027acd3900146e8f5096bcc Merge: Abstract attribute Add the annotation `abstract` on attributes. It is just a syntactic sugar to define a couple of abstract getter-setters with a shared documentation without an associated slot in the instance so it can be used in interfaces. ~~~nit interface Foo var a: Object is abstract end class Bar super Foo # A concrete attribute that redefine the abstract one redef var a end class Baz super Foo var real_a: Object # A pair of concrete methods that redefine the abstract attribute redef fun a do return real_a redef fun a=(x) do real_a = x end ~~~ The visibility rules are unchanged with regard to concrete attributes, so by default the writer is private. Needed cleaning (and a bugfix) are included in the PR Pull-Request: #1177 Reviewed-by: Lucas Bajolet Reviewed-by: Alexandre Terrasa Reviewed-by: Alexis Laferrière --- diff --git a/VERSION b/VERSION index 63f2359..2c0a9c7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v0.7.1 +v0.7.2 diff --git a/contrib/jwrapper/src/model.nit b/contrib/jwrapper/src/model.nit index 34792c9..d38a7a1 100644 --- a/contrib/jwrapper/src/model.nit +++ b/contrib/jwrapper/src/model.nit @@ -230,9 +230,9 @@ class JavaType return id end - fun collections_list: Array[String] is cached do return ["List", "ArrayList", "LinkedList", "Vector", "Set", "SortedSet", "HashSet", "TreeSet", "LinkedHashSet", "Map", "SortedMap", "HashMap", "TreeMap", "Hashtable", "LinkedHashMap"] - fun iterable: Array[String] is cached do return ["ArrayList", "Set", "HashSet", "LinkedHashSet", "LinkedList", "Stack", "TreeSet", "Vector"] - fun maps: Array[String] is cached do return ["Map", "SortedMap", "HashMap", "TreeMap", "Hashtable", "LinkedHashMap"] + var collections_list: Array[String] is lazy do return ["List", "ArrayList", "LinkedList", "Vector", "Set", "SortedSet", "HashSet", "TreeSet", "LinkedHashSet", "Map", "SortedMap", "HashMap", "TreeMap", "Hashtable", "LinkedHashMap"] + var iterable: Array[String] is lazy do return ["ArrayList", "Set", "HashSet", "LinkedHashSet", "LinkedList", "Stack", "TreeSet", "Vector"] + var maps: Array[String] is lazy do return ["Map", "SortedMap", "HashMap", "TreeMap", "Hashtable", "LinkedHashMap"] end class NitType diff --git a/contrib/nitiwiki/src/wiki_base.nit b/contrib/nitiwiki/src/wiki_base.nit index fa78818..1527d4c 100644 --- a/contrib/nitiwiki/src/wiki_base.nit +++ b/contrib/nitiwiki/src/wiki_base.nit @@ -308,7 +308,7 @@ abstract class WikiEntry # Result is returned as an array containg ordered entries: # `breadcrumbs.first` is the root entry and # `breadcrumbs.last == self` - fun breadcrumbs: Array[WikiEntry] is cached do + var breadcrumbs: Array[WikiEntry] is lazy do var path = new Array[WikiEntry] var entry: nullable WikiEntry = self while entry != null and not entry.is_root do @@ -538,7 +538,7 @@ class WikiArticle # Extract the markdown text from `source_file`. # # REQUIRE: `has_source`. - fun md: String is cached do + var md: String is lazy do assert has_source var file = new FileReader.open(src_full_path.to_s) var md = file.read_all @@ -577,7 +577,7 @@ class WikiConfig # # * key: `wiki.name` # * default: `MyWiki` - fun wiki_name: String is cached do return value_or_default("wiki.name", "MyWiki") + var wiki_name: String is lazy do return value_or_default("wiki.name", "MyWiki") # Site description. # @@ -585,7 +585,7 @@ class WikiConfig # # * key: `wiki.desc` # * default: `` - fun wiki_desc: String is cached do return value_or_default("wiki.desc", "") + var wiki_desc: String is lazy do return value_or_default("wiki.desc", "") # Site logo url. # @@ -593,13 +593,13 @@ class WikiConfig # # * key: `wiki.logo` # * default: `` - fun wiki_logo: String is cached do return value_or_default("wiki.logo", "") + var wiki_logo: String is lazy do return value_or_default("wiki.logo", "") # Root url of the wiki. # # * key: `wiki.root_url` # * default: `http://localhost/` - fun root_url: String is cached do return value_or_default("wiki.root_url", "http://localhost/") + var root_url: String is lazy do return value_or_default("wiki.root_url", "http://localhost/") # Root directory of the wiki. @@ -608,7 +608,7 @@ class WikiConfig # # * key: `wiki.root_dir` # * default: `./` - fun root_dir: String is cached do return value_or_default("wiki.root_dir", "./").simplify_path + var root_dir: String is lazy do return value_or_default("wiki.root_dir", "./").simplify_path # Pages directory. # @@ -616,7 +616,7 @@ class WikiConfig # # * key: `wiki.source_dir # * default: `pages/` - fun source_dir: String is cached do + var source_dir: String is lazy do return value_or_default("wiki.source_dir", "pages/").simplify_path end @@ -627,7 +627,7 @@ class WikiConfig # # * key: `wiki.out_dir` # * default: `out/` - fun out_dir: String is cached do return value_or_default("wiki.out_dir", "out/").simplify_path + var out_dir: String is lazy do return value_or_default("wiki.out_dir", "out/").simplify_path # Asset files directory. # @@ -636,7 +636,7 @@ class WikiConfig # # * key: `wiki.assets_dir` # * default: `assets/` - fun assets_dir: String is cached do + var assets_dir: String is lazy do return value_or_default("wiki.assets_dir", "assets/").simplify_path end @@ -647,7 +647,7 @@ class WikiConfig # # * key: `wiki.templates_dir` # * default: `templates/` - fun templates_dir: String is cached do + var templates_dir: String is lazy do return value_or_default("wiki.templates_dir", "templates/").simplify_path end @@ -657,7 +657,7 @@ class WikiConfig # # * key: `wiki.template` # * default: `template.html` - fun template_file: String is cached do + var template_file: String is lazy do return value_or_default("wiki.template", "template.html") end @@ -668,7 +668,7 @@ class WikiConfig # # * key: `wiki.header` # * default: `header.html` - fun header_file: String is cached do + var header_file: String is lazy do return value_or_default("wiki.header", "header.html") end @@ -678,7 +678,7 @@ class WikiConfig # # * key: `wiki.menu` # * default: `menu.html` - fun menu_file: String is cached do + var menu_file: String is lazy do return value_or_default("wiki.menu", "menu.html") end @@ -689,7 +689,7 @@ class WikiConfig # # * key: `wiki.footer` # * default: `footer.html` - fun footer_file: String is cached do + var footer_file: String is lazy do return value_or_default("wiki.footer", "footer.html") end @@ -699,19 +699,19 @@ class WikiConfig # # * key: `wiki.rsync_dir` # * default: `` - fun rsync_dir: String is cached do return value_or_default("wiki.rsync_dir", "") + var rsync_dir: String is lazy do return value_or_default("wiki.rsync_dir", "") # Remote repository used to pull modifications on sources. # # * key: `wiki.git_origin` # * default: `origin` - fun git_origin: String is cached do return value_or_default("wiki.git_origin", "origin") + var git_origin: String is lazy do return value_or_default("wiki.git_origin", "origin") # Remote branch used to pull modifications on sources. # # * key: `wiki.git_branch` # * default: `master` - fun git_branch: String is cached do return value_or_default("wiki.git_branch", "master") + var git_branch: String is lazy do return value_or_default("wiki.git_branch", "master") end # WikiSection custom configuration. diff --git a/contrib/nitiwiki/src/wiki_html.nit b/contrib/nitiwiki/src/wiki_html.nit index 10ab09b..3427016 100644 --- a/contrib/nitiwiki/src/wiki_html.nit +++ b/contrib/nitiwiki/src/wiki_html.nit @@ -107,7 +107,7 @@ redef class WikiSection # # If no file `index.md` exists for this section, # a summary is generated using contained articles. - fun index: WikiArticle is cached do + var index: WikiArticle is lazy do for child in children.values do if child isa WikiArticle and child.is_index then return child end diff --git a/contrib/nitrpg/src/achievements.nit b/contrib/nitrpg/src/achievements.nit new file mode 100644 index 0000000..625bfc2 --- /dev/null +++ b/contrib/nitrpg/src/achievements.nit @@ -0,0 +1,402 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2014-2015 Alexandre Terrasa +# +# 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. + +# `nitrpg` achievements. +# +# Players can unlock achievements by performing remarkable actions on the repo. +# Achievements are rewarded by nitcoins. +module achievements + +import events +import statistics + +redef class GameEntity + + # Register a new achievement for this game entity. + # + # Saves the achievement in game data. + # Do nothing is the achievement is already registered. + # + # TODO should update the achievement? + fun add_achievement(achievement: Achievement) do + var key = self.key / achievement.key + if game.store.has_key(key) then return + stats.inc("achievements") + achievement.save_in(self) + save + end + + # Load the event from its `id`. + # + # Looks for the event save file in game data. + # Returns `null` if the event cannot be found. + fun load_achievement(id: String): nullable Achievement do + var key = self.key / "achievements" / id + if not game.store.has_key(key) then return null + var json = game.store.load_object(key) + return new Achievement.from_json(game, json) + end + + # List all events registered in this entity. + # + # This list is reloaded from game data each time its called. + # + # To add events see `add_event`. + fun load_achievements: MapRead[String, Achievement] do + var res = new HashMap[String, Achievement] + var key = self.key / "achievements" + if not game.store.has_collection(key) then return res + var coll = game.store.list_collection(key) + for id in coll do + res[id.to_s] = load_achievement(id.to_s).as(not null) + end + return res + end +end + +# Achievements are rewarded by `nitcoins`. +# +# An achievement represents a notable action performed by a `Player`. +# Player that `unlock` achievements are rewarded by nitcoins. +class Achievement + super GameEntity + + redef var key is lazy do return "achievements" / id + + redef var game + + # Uniq ID for this achievement. + var id: String + + # Name of this achievement. + var name: String + + # Description of the achievement. + var desc: String + + # Reward that this achievement give in nitcoins. + var reward: Int + + # Is this achievement unlocked by somebody? + var is_unlocked: Bool is lazy do return not load_events.is_empty + + # Init `self` from a `json` object. + # + # Used to load achievements from storage. + init from_json(game: Game, json: JsonObject) do + self.game = game + id = json["id"].to_s + name = json["name"].to_s + desc = json["desc"].to_s + reward = json["reward"].as(Int) + end + + redef fun to_json do + var json = super + json["id"] = id + json["name"] = name + json["desc"] = desc + json["reward"] = reward + return json + end +end + +redef class Player + + # Is `a` unlocked for this `Player`? + fun has_achievement(a: Achievement): Bool do + return load_achievement(a.id) != null + end + + # Unlocks an achievement for this Player based on a GithubEvent. + # + # Register the achievement and adds the achievement reward to the player + # nitcoins. + # + # Do nothing is this player has already unlocked the achievement. + # + # TODO: add abstraction so achievements do not depend on GithubEvent. + fun unlock_achievement(a: Achievement, event: GithubEvent) do + if has_achievement(a) then return + nitcoins += a.reward + add_achievement(a) + trigger_unlock_event(a, event) + end + + # Create a new event that marks the achievement unlocking. + fun trigger_unlock_event(achievement: Achievement, event: GithubEvent) do + var obj = new JsonObject + obj["player"] = name + obj["reward"] = achievement.reward + obj["achievement"] = achievement.id + obj["github_event"] = event.json + var ge = new GameEvent(game, "achievement_unlocked", obj) + add_event(ge) + game.add_event(ge) + achievement.add_event(ge) + end +end + +# `GameReactor` dedicated to achievements unlocking. +interface AchievementReactor + super GameReactor + + # Unic ID of the achievement this reactor unlocks. + fun id: String is abstract + + # Name of the achievement this reactor unlocks. + fun name: String is abstract + + # Description of the achievement this reactor unlocks. + fun desc: String is abstract + + # Amount of nitcoins rewarded for unlocking the achievement. + fun reward: Int is abstract + + # Return a new instance of the achievement to unlock. + fun new_achievement(game: Game): Achievement do + var achievement = new Achievement(game, id, name, desc, reward) + game.add_achievement(achievement) + return achievement + end +end + +##################### +### Issues +##################### + +# Unlock achievement after X issues. +# +# Used to factorize behavior. +abstract class PlayerXIssues + super AchievementReactor + + # Number of PR required to unlock the achievement. + var threshold: Int is noinit + + redef fun react_event(game, event) do + if not event isa IssuesEvent then return + if not event.action == "opened" then return + var player = event.issue.user.player(game) + if player.stats["issues"] == threshold then + var a = new_achievement(game) + player.unlock_achievement(a, event) + end + end +end + +# Player open his first issue. +class Player1Issue + super PlayerXIssues + + redef var id = "player_1_issue" + redef var name = "First complaint" + redef var desc = "Open your first issue." + redef var reward = 10 + redef var threshold = 1 +end + +# Player open 100 issues. +class Player100Issues + super PlayerXIssues + + redef var id = "player_100_issues" + redef var name = "Mature whiner" + redef var desc = "Open 100 issues in the game." + redef var reward = 100 + redef var threshold = 100 +end + +# Player open 1 000 issues. +class Player1KIssues + super PlayerXIssues + + redef var id = "player_1000_issues" + redef var name = "You, sir, complain a lot" + redef var desc = "Open 1000 issues in the game." + redef var reward = 1000 + redef var threshold = 1000 +end + +# Player open an issue about nitdoc. +class IssueAboutNitdoc + super AchievementReactor + + redef var id = "issue_about_nitdoc" + redef var name = "Say nitdoc again, I double dare you!" + redef var desc = "Open an issue with \"nitdoc\" in the title." + redef var reward = 10 + + redef fun react_event(game, event) do + if not event isa IssuesEvent then return + if not event.action == "opened" then return + var player = event.issue.user.player(game) + var re = "nitdoc".to_re + re.ignore_case = true + if event.issue.title.has(re) then + var a = new_achievement(game) + player.unlock_achievement(a, event) + end + end +end + +# Player open an issue about FFI. +class IssueAboutFFI + super PlayerXIssues + + redef var id = "issue_about_ffi" + redef var name = "Polyglot what?" + redef var desc = "Open an issue with `ffi` in the title." + redef var reward = 10 + + redef fun react_event(game, event) do + if not event isa IssuesEvent then return + if not event.action == "opened" then return + var player = event.issue.user.player(game) + var re = "\\bffi\\b".to_re + re.ignore_case = true + if event.issue.title.has(re) then + var a = new_achievement(game) + player.unlock_achievement(a, event) + end + end +end + +##################### +### Pull requests +##################### + +# Unlock achievement after X pull requests. +# +# Used to factorize behavior. +abstract class PlayerXPulls + super AchievementReactor + + # Number of PR required to unlock the achievement. + var threshold: Int is noinit + + redef fun react_event(game, event) do + if not event isa PullRequestEvent then return + if not event.action == "opened" then return + var player = event.pull.user.player(game) + if player.stats["pulls"] == threshold then + var a = new_achievement(game) + player.unlock_achievement(a, event) + end + end +end + +# Open your first pull request. +class Player1Pull + super PlayerXPulls + + redef var id = "player_1_pull" + redef var name = "First PR" + redef var desc = "Open your first pull request." + redef var reward = 10 + redef var threshold = 1 +end + +# Author 100 pull requests. +class Player100Pulls + super PlayerXPulls + + redef var id = "player_100_pulls" + redef var name = "100 pull requests!!!" + redef var desc = "Open 100 pull requests in the game." + redef var reward = 100 + redef var threshold = 100 +end + +# Author 1000 pull requests. +class Player1KPulls + super PlayerXPulls + + redef var id = "player_1000_pulls" + redef var name = "1000 PULL REQUESTS!!!" + redef var desc = "Open 1000 pull requests in the game." + redef var reward = 1000 + redef var threshold = 1000 +end + +##################### +### Commits +##################### + +# Unlock achievement after X merged commits. +# +# Used to factorize behavior. +abstract class PlayerXCommits + super AchievementReactor + + # Number of PR required to unlock the achievement. + var threshold: Int is noinit + + redef fun react_event(game, event) do + if not event isa PullRequestEvent then return + if not event.action == "closed" then return + if not event.pull.merged then return + var player = event.pull.user.player(game) + if player.stats["commits"] == threshold then + var a = new_achievement(game) + player.unlock_achievement(a, event) + end + end +end + +# Author your first commit in the game. +class Player1Commit + super PlayerXCommits + + redef var id = "player_1_commit" + redef var name = "First blood" + redef var desc = "Author your first commit in the game." + redef var reward = 10 + redef var threshold = 1 +end + +# Author 100 commits. +class Player100Commits + super PlayerXCommits + + redef var id = "player_100_commits" + redef var name = "100 commits" + redef var desc = "Author 100 commits in the game." + redef var reward = 100 + redef var threshold = 100 +end + +# Author 1 000 commits. +class Player1KCommits + super PlayerXCommits + + redef var id = "player_1000_commits" + redef var name = "1000 commits!!!" + redef var desc = "Author 1000 commits in the game." + redef var reward = 1000 + redef var threshold = 1000 +end + +# Author 10 000 commits. +class Player10KCommits + super PlayerXCommits + + redef var id = "player_10000_commits" + redef var name = "10000 COMMITS!!!" + redef var desc = "Author 10000 commits in the game." + redef var reward = 10000 + redef var threshold = 10000 +end diff --git a/contrib/nitrpg/src/game.nit b/contrib/nitrpg/src/game.nit index 9c71566..4d62283 100644 --- a/contrib/nitrpg/src/game.nit +++ b/contrib/nitrpg/src/game.nit @@ -235,11 +235,16 @@ end redef class User # The player linked to `self`. - fun player(game: Game): Player is lazy do - var player = game.load_player(login) + fun player(game: Game): Player do + var player = player_cache.get_or_null(game) + if player != null then return player + player = game.load_player(login) if player == null then player = game.add_player(self) + player_cache[game] = player return player end + + private var player_cache = new HashMap[Game, Player] end # A GameReactor reacts to event sent by a `Github::HookListener`. diff --git a/contrib/nitrpg/src/listener.nit b/contrib/nitrpg/src/listener.nit index d35b169..0e4ba00 100644 --- a/contrib/nitrpg/src/listener.nit +++ b/contrib/nitrpg/src/listener.nit @@ -17,8 +17,8 @@ # This tool is runned to listen to `Github::Event` and update the game. module listener -import statistics import reactors +import achievements import github::hooks # `HookListener` that redirects events to a `Game` instance. @@ -34,8 +34,14 @@ class RpgHookListener # TODO handle verbosity with opts game.verbose_lvl = 1 game.message(1, "Received event {event} for {game.repo.full_name}") - for reactor in reactors do reactor.react_event(game, event) + for reactor in reactors do + game.message(2, "Apply reactor {reactor} on {event}") + reactor.react_event(game, event) + end end + + # Register a reactor for this listener. + fun add_reactor(reactors: GameReactor...) do self.reactors.add_all reactors end if args.length != 2 then @@ -51,9 +57,12 @@ var port = args[1].to_i var api = new GithubAPI(get_github_oauth) -var listener = new RpgHookListener(api, host, port) -listener.reactors.add new StatisticsReactor -listener.reactors.add new PlayerReactor +var l = new RpgHookListener(api, host, port) +l.add_reactor(new StatisticsReactor, new PlayerReactor) +l.add_reactor(new Player1Issue, new Player100Issues, new Player1KIssues) +l.add_reactor(new Player1Pull, new Player100Pulls, new Player1KPulls) +l.add_reactor(new Player1Commit, new Player100Commits, new Player1KCommits) +l.add_reactor(new IssueAboutNitdoc, new IssueAboutFFI) print "Listening events on {host}:{port}" -listener.listen +l.listen diff --git a/contrib/nitrpg/src/statistics.nit b/contrib/nitrpg/src/statistics.nit index 39c05af..dfdb3ba 100644 --- a/contrib/nitrpg/src/statistics.nit +++ b/contrib/nitrpg/src/statistics.nit @@ -26,6 +26,9 @@ import counter redef class GameEntity + # Statistics for this entity. + fun stats: GameStats is abstract + # Load statistics for this `MEntity` if any. fun load_statistics: nullable GameStats do var key = self.key / "statistics" @@ -33,23 +36,17 @@ redef class GameEntity var json = game.store.load_object(key) return new GameStats.from_json(game, json) end - - # Save statistics under this `MEntity`. - fun save_statistics(stats: GameStats) do - game.store.store_object(key / stats.key, stats.to_json) - end end redef class Game - # Statistics for this game instance. - var stats: GameStats is lazy do + redef var stats is lazy do return load_statistics or else new GameStats(game) end redef fun save do super - save_statistics(stats) + stats.save_in(self) end redef fun pretty do @@ -63,14 +60,13 @@ end redef class Player - # Statistics for this player. - var stats: GameStats is lazy do + redef var stats is lazy do return load_statistics or else new GameStats(game) end redef fun save do super - save_statistics(stats) + stats.save_in(self) end redef fun pretty do @@ -125,9 +121,7 @@ end class StatisticsReactor super GameReactor - redef fun react_event(game, e) do - e.react_stats_event(game) - end + redef fun react_event(game, e) do e.react_stats_event(game) end redef class GithubEvent diff --git a/contrib/nitrpg/src/templates/panels.nit b/contrib/nitrpg/src/templates/panels.nit index 89d6431..14538c5 100644 --- a/contrib/nitrpg/src/templates/panels.nit +++ b/contrib/nitrpg/src/templates/panels.nit @@ -112,7 +112,9 @@ class GameStatusPanel redef fun render_body do add "{game.load_players.length}" - add " players

" + add " players
" + add "{game.stats["achievements"]}" + add " achievements

" add "{game.stats["pulls"]} pull requests" add " ({game.stats["pulls_open"]} open)
" add "{game.stats["issues"]} issues" @@ -144,6 +146,7 @@ class PlayerStatusPanel add "

ranked " add " # {ranking[player.name]}

" add "{player.nitcoins} nitcoins

" + add "{player.stats["achievements"]} achievements

" add "{player.stats["pulls"]} pull requests
" add "{player.stats["issues"]} issues
" add "{player.stats["commits"]} commits" @@ -360,3 +363,75 @@ class EventListPanel add "" end end + +# Achievement unlocked list panel. +class AchievementsListPanel + super Panel + + # Entity to load the events from. + var entity: GameEntity + + redef fun render_title do + add "  " + add "Achievements unlocked" + end + + redef fun render_body do + var achs = entity.load_achievements.values.to_a + if achs.is_empty then + add "No achievement yet..." + return + end + for ach in achs do add ach.list_item + end +end + +# Achievement detail panel. +class AchievementPanel + super Panel + + # Achievement to display. + var achievement: Achievement + + redef fun render_title do + add "  " + add "Achievement details" + end + + redef fun render_body do + add """

+ +{{{achievement.reward}}} + {{{achievement.name}}} +

+

{{{achievement.desc}}}

""" + + var events = achievement.load_events + + if events.is_empty then + add "Never unlocked..." + return + end + + var event = events.last + var tpl = event.tpl_event + var player = tpl.player + add "
" + add """
+ + #1 + {{{player.name}}} + +
+

Unlocked first by {{{player.link}}}

+ at {{{event.time}}} +
+
""" + + if events.length > 1 then + add """


Also unlocked by + {{{events.length}}} players.

""" + end + end +end diff --git a/contrib/nitrpg/src/templates/templates.nit b/contrib/nitrpg/src/templates/templates.nit index f1affb9..92a4915 100644 --- a/contrib/nitrpg/src/templates/templates.nit +++ b/contrib/nitrpg/src/templates/templates.nit @@ -54,7 +54,7 @@ class NitRpgPage