580023a60b9068b1c6804e6e87d239fb2ca4bf3a
[nit.git] / contrib / nitrpg / src / statistics.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2014-2015 Alexandre Terrasa <alexandre@moz-code.org>
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 # Statistics about the Game.
18 #
19 # This module uses `GameReactor` to extract statistics about the game from
20 # triggered `Github::Event`.
21 module statistics
22
23 import game
24 import github::hooks
25 import counter
26
27 redef class GameEntity
28
29 # Statistics manager for this entity.
30 fun stats: GameStatsManager is abstract
31 end
32
33 redef class Game
34
35 redef var stats is lazy do return new GameStatsManager(game, self)
36
37 redef fun save do
38 super
39 stats.save_in(self.key)
40 end
41
42 redef fun pretty do
43 var res = new FlatBuffer
44 res.append super
45 res.append "# stats:\n"
46 res.append stats.pretty
47 return res.write_to_string
48 end
49 end
50
51 redef class Player
52
53 redef var stats is lazy do return new GameStatsManager(game, self)
54
55 redef fun save do
56 super
57 stats.save_in(self.key)
58 end
59
60 redef fun pretty do
61 var res = new FlatBuffer
62 res.append super
63 res.append "# stats:\n"
64 res.append stats.pretty
65 return res.write_to_string
66 end
67 end
68
69 # Store game stats for defined period.
70 class GameStatsManager
71 super GameEntity
72 super Counter[String]
73
74 redef var game
75
76 # The GameEntity monitored by these statistics.
77 var owner: GameEntity
78
79 redef var key = "stats"
80
81 # Returns the `GameStats` instance for the overall statistics.
82 var overall: GameStats is lazy do
83 return load_stats_for("all")
84 end
85
86 # Returns the `GameStats` instance for the current year statistics.
87 var yearly: GameStats is lazy do
88 var date = new Tm.gmtime
89 var key = date.strftime("%Y")
90 return load_stats_for(key)
91 end
92
93 # Returns the `GameStats` instance for the current month statistics.
94 var monthly: GameStats is lazy do
95 var date = new Tm.gmtime
96 var key = date.strftime("%Y-%m")
97 return load_stats_for(key)
98 end
99
100 # Returns the `GameStats` instance for the current day statistics.
101 var daily: GameStats is lazy do
102 var date = new Tm.gmtime
103 var key = date.strftime("%Y-%m-%d")
104 return load_stats_for(key)
105 end
106
107 # Returns the `GameStats` instance for the current week statistics.
108 var weekly: GameStats is lazy do
109 var date = new Tm.gmtime
110 var key = date.strftime("%Y-W%U")
111 return load_stats_for(key)
112 end
113
114 # Load statistics for a `period` key.
115 fun load_stats_for(period: String): GameStats do
116 var key = owner.key / self.key / period
117 if not game.store.has_key(key) then
118 return new GameStats(game, period)
119 end
120 var json = game.store.load_object(key)
121 return new GameStats.from_json(game, period, json)
122 end
123
124 redef fun [](key) do return overall[key]
125
126 redef fun []=(key, value) do
127 overall[key] = value
128 yearly[key] = value
129 monthly[key] = value
130 daily[key] = value
131 weekly[key] = value
132 end
133
134 redef fun inc(e) do
135 overall.inc(e)
136 yearly.inc(e)
137 monthly.inc(e)
138 daily.inc(e)
139 weekly.inc(e)
140 end
141
142 redef fun dec(e) do
143 overall.dec(e)
144 yearly.dec(e)
145 monthly.dec(e)
146 daily.dec(e)
147 weekly.dec(e)
148 end
149
150 redef fun save_in(key) do
151 overall.save_in(key / self.key)
152 yearly.save_in(key / self.key)
153 monthly.save_in(key / self.key)
154 daily.save_in(key / self.key)
155 weekly.save_in(key / self.key)
156 end
157
158 redef fun pretty do return overall.pretty
159 end
160
161 # Game statistics structure that can be saved as a `GameEntity`.
162 class GameStats
163 super GameEntity
164 super Counter[String]
165
166 redef var game
167
168 # The pedriod these stats are about.
169 var period: String
170
171 redef fun key do return period
172
173 # Load `self` from saved data.
174 init from_json(game: Game, period: String, json: JsonObject) do
175 for k, v in json do self[k] = v.as(Int)
176 end
177
178 redef fun to_json do
179 var obj = new JsonObject
180 for k, v in self do obj[k] = v
181 return obj
182 end
183
184 redef fun pretty do
185 var res = new FlatBuffer
186 for k, v in self do
187 res.append "# {v} {k}\n"
188 end
189 return res.write_to_string
190 end
191 end
192
193 # `GameReactor` that computes statistics about the game.
194 class StatisticsReactor
195 super GameReactor
196
197 redef fun react_event(game, e) do e.react_stats_event(game)
198 end
199
200 redef class GithubEvent
201 # Reacts to a statistics related event.
202 #
203 # Called by `StatisticsReactor::react_event`.
204 # No-op by default.
205 private fun react_stats_event(game: Game) do end
206 end
207
208 redef class IssuesEvent
209
210 # Count opened and closed issues.
211 redef fun react_stats_event(game) do
212 var player = issue.user.player(game)
213 if action == "opened" then
214 game.stats.inc("issues")
215 game.stats.inc("issues_open")
216 game.save
217 player.stats.inc("issues")
218 player.stats.inc("issues_open")
219 player.save
220 else if action == "reopened" then
221 game.stats.inc("issues_open")
222 game.save
223 player.stats.inc("issues_open")
224 player.save
225 else if action == "closed" then
226 game.stats.dec("issues_open")
227 game.save
228 player.stats.dec("issues_open")
229 player.save
230 end
231 end
232 end
233
234 redef class PullRequestEvent
235
236 # Count opened and closed pull requests.
237 redef fun react_stats_event(game) do
238 var player = pull.user.player(game)
239 if action == "opened" then
240 game.stats.inc("pulls")
241 game.stats.inc("pulls_open")
242 game.save
243 player.stats.inc("pulls")
244 player.stats.inc("pulls_open")
245 player.save
246 else if action == "reopened" then
247 game.stats.inc("pulls_open")
248 game.save
249 player.stats.inc("pulls_open")
250 player.save
251 else if action == "closed" then
252 game.stats.dec("pulls_open")
253 player.stats.dec("pulls_open")
254 if pull.merged then
255 game.stats["commits"] += pull.commits
256 player.stats["commits"] += pull.commits
257 end
258 game.save
259 player.save
260 end
261 end
262 end
263
264 redef class IssueCommentEvent
265
266 # Count posted comments
267 redef fun react_stats_event(game) do
268 if action == "created" then
269 var player = comment.user.player(game)
270 game.stats.inc("comments")
271 player.stats.inc("comments")
272 # FIXME use a more precise way to locate reviews
273 if comment.has_ok_review then
274 game.stats.inc("reviews")
275 player.stats.inc("reviews")
276 end
277 game.save
278 player.save
279 end
280 end
281 end
282
283 redef class IssueComment
284 # Does this comment contain a "+1"?
285 fun has_ok_review: Bool do return body.has("\\+1\\b".to_re)
286 end