lib/github: handles labels
[nit.git] / lib / github / api.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # Nit object oriented interface to Github api.
16 #
17 # This modules reifies Github API elements as Nit classes.
18 #
19 # For most use-cases you need to use the `GithubAPI` client.
20 module api
21
22 import github_curl
23
24 # Interface to Github REST API.
25 #
26 # Used by all `GithubEntity` to perform requests.
27 #
28 # Usage:
29 #
30 # ~~~
31 # # Get Github authentification token.
32 # var token = get_github_oauth
33 # assert not token.is_empty
34 #
35 # # Init the client.
36 # var api = new GithubAPI(token)
37 # ~~~
38 #
39 # The API client allows to get Github API entities:
40 #
41 # ~~~
42 # var repo = api.load_repo("privat/nit")
43 # assert repo isa Repo
44 # assert repo.name == "nit"
45 #
46 # var user = api.load_user("Morriar")
47 # assert user isa User
48 # assert user.login == "Morriar"
49 # ~~~
50 class GithubAPI
51
52 # Github API OAuth token.
53 #
54 # This token is used to authenticate the application on Github API.
55 # Be aware that there is [rate limits](https://developer.github.com/v3/rate_limit/)
56 # associated to the key.
57 var auth: String
58
59 # Github API base url.
60 #
61 # Default is `https://api.github.com` and should not be changed.
62 var api_url = "https://api.github.com"
63
64 # User agent used for HTTP requests.
65 #
66 # Default is `nit_github_api`.
67 #
68 # See <https://developer.github.com/v3/#user-agent-required>
69 var user_agent = "nit_github_api"
70
71 # Curl instance.
72 #
73 # Internal Curl instance used to perform API calls.
74 private var ghcurl: GithubCurl is noinit
75
76 # Verbosity level.
77 #
78 # * `0`: only errors (default)
79 # * `1`: verbose
80 var verbose_lvl = 0 is public writable
81
82 init do
83 ghcurl = new GithubCurl(auth, user_agent)
84 end
85
86 # Execute a GET request on Github API.
87 #
88 # This method returns raw json data.
89 # See other `load_*` methods to use more expressive types.
90 #
91 # var api = new GithubAPI(get_github_oauth)
92 # var obj = api.get("repos/privat/nit")
93 # assert obj isa JsonObject
94 # assert obj["name"] == "nit"
95 #
96 # Returns `null` in case of `error`.
97 #
98 # obj = api.get("foo/bar/baz")
99 # assert obj == null
100 # assert api.was_error
101 # var err = api.last_error
102 # assert err isa GithubError
103 # assert err.name == "GithubAPIError"
104 # assert err.message == "Not Found"
105 fun get(path: String): nullable Jsonable do
106 path = sanitize_uri(path)
107 var res = ghcurl.get_and_parse("{api_url}/{path}")
108 if res isa Error then
109 last_error = res
110 was_error = true
111 return null
112 end
113 was_error = false
114 return res
115 end
116
117 # Display a message depending on `verbose_lvl`.
118 fun message(lvl: Int, message: String) do
119 if lvl <= verbose_lvl then print message
120 end
121
122 # Escape `uri` in an acceptable format for Github.
123 private fun sanitize_uri(uri: String): String do
124 # TODO better URI escape.
125 return uri.replace(" ", "%20")
126 end
127
128 # Last error occured during Github API communications.
129 var last_error: nullable Error = null is public writable
130
131 # Does the last request provoqued an error?
132 var was_error = false is protected writable
133
134 # Load the json object from Github.
135 # See `GithubEntity::load_from_github`.
136 private fun load_from_github(key: String): JsonObject do
137 message(1, "Get {key} (github)")
138 var res = get(key)
139 if was_error then return new JsonObject
140 return res.as(JsonObject)
141 end
142
143 # Get the Github user with `login`.
144 #
145 # Returns `null` if the user cannot be found.
146 #
147 # var api = new GithubAPI(get_github_oauth)
148 # var user = api.load_user("Morriar")
149 # assert user.login == "Morriar"
150 fun load_user(login: String): nullable User do
151 var user = new User(self, login)
152 user.load_from_github
153 if was_error then return null
154 return user
155 end
156
157 # Get the Github repo with `full_name`.
158 #
159 # Returns `null` if the repo cannot be found.
160 #
161 # var api = new GithubAPI(get_github_oauth)
162 # var repo = api.load_repo("privat/nit")
163 # assert repo.name == "nit"
164 # assert repo.owner.login == "privat"
165 # assert repo.default_branch.name == "master"
166 fun load_repo(full_name: String): nullable Repo do
167 var repo = new Repo(self, full_name)
168 repo.load_from_github
169 if was_error then return null
170 return repo
171 end
172
173 # Get the Github branch with `name`.
174 #
175 # Returns `null` if the branch cannot be found.
176 #
177 # var api = new GithubAPI(get_github_oauth)
178 # var repo = api.load_repo("privat/nit")
179 # assert repo isa Repo
180 # var branch = api.load_branch(repo, "master")
181 # assert branch.name == "master"
182 # assert branch.commit isa Commit
183 fun load_branch(repo: Repo, name: String): nullable Branch do
184 var branch = new Branch(self, repo, name)
185 branch.load_from_github
186 if was_error then return null
187 return branch
188 end
189
190 # Get the Github commit with `sha`.
191 #
192 # Returns `null` if the commit cannot be found.
193 #
194 # var api = new GithubAPI(get_github_oauth)
195 # var repo = api.load_repo("privat/nit")
196 # assert repo isa Repo
197 # var commit = api.load_commit(repo, "64ce1f")
198 # assert commit isa Commit
199 fun load_commit(repo: Repo, sha: String): nullable Commit do
200 var commit = new Commit(self, repo, sha)
201 commit.load_from_github
202 if was_error then return null
203 return commit
204 end
205
206 # Get the Github label with `name`.
207 #
208 # Returns `null` if the label cannot be found.
209 #
210 # var api = new GithubAPI(get_github_oauth)
211 # var repo = api.load_repo("privat/nit")
212 # assert repo isa Repo
213 # var labl = api.load_label(repo, "ok_will_merge")
214 # assert labl != null
215 fun load_label(repo: Repo, name: String): nullable Label do
216 var labl = new Label(self, repo, name)
217 labl.load_from_github
218 if was_error then return null
219 return labl
220 end
221 end
222
223 # Something returned by the Github API.
224 #
225 # Mainly a Nit wrapper around a JSON objet.
226 abstract class GithubEntity
227
228 # Github API instance.
229 var api: GithubAPI
230
231 # FIXME constructor should be private
232
233 # Key used to access this entity from Github api base.
234 fun key: String is abstract
235
236 # JSON representation of `self`.
237 #
238 # This is the same json structure than used by Github API.
239 var json: JsonObject is noinit, protected writable
240
241 # Load `json` from Github API.
242 private fun load_from_github do
243 json = api.load_from_github(key)
244 end
245
246 redef fun to_s do return json.to_json
247 end
248
249 # A Github user.
250 #
251 # Should be accessed from `GithubAPI::load_user`.
252 #
253 # See <https://developer.github.com/v3/users/>.
254 class User
255 super GithubEntity
256
257 redef var key is lazy do return "users/{login}"
258
259 # Github login.
260 var login: String
261
262 # Init `self` from a `json` object.
263 init from_json(api: GithubAPI, json: JsonObject) do
264 init(api, json["login"].to_s)
265 self.json = json
266 end
267
268 # Github User page url.
269 fun html_url: String do return json["html_url"].to_s
270
271 # Avatar image url for this user.
272 fun avatar_url: String do return json["avatar_url"].to_s
273 end
274
275 # A Github repository.
276 #
277 # Should be accessed from `GithubAPI::load_repo`.
278 #
279 # See <https://developer.github.com/v3/repos/>.
280 class Repo
281 super GithubEntity
282
283 redef var key is lazy do return "repos/{full_name}"
284
285 # Repo full name on Github.
286 var full_name: String
287
288 # Init `self` from a `json` object.
289 init from_json(api: GithubAPI, json: JsonObject) do
290 init(api, json["full_name"].to_s)
291 self.json = json
292 end
293
294 # Repo short name on Github.
295 fun name: String do return json["name"].to_s
296
297 # Github User page url.
298 fun html_url: String do return json["html_url"].to_s
299
300 # Get the repo owner.
301 fun owner: User do
302 return new User.from_json(api, json["owner"].as(JsonObject))
303 end
304
305 # List of branches associated with their names.
306 fun branches: Map[String, Branch] do
307 api.message(1, "Get branches for {full_name}")
308 var array = api.get("repos/{full_name}/branches")
309 var res = new HashMap[String, Branch]
310 if not array isa JsonArray then return res
311 for obj in array do
312 if not obj isa JsonObject then continue
313 var name = obj["name"].to_s
314 res[name] = new Branch.from_json(api, self, obj)
315 end
316 return res
317 end
318
319 # List of labels associated with their names.
320 fun labels: Map[String, Label] do
321 api.message(1, "Get labels for {full_name}")
322 var array = api.get("repos/{full_name}/labels")
323 var res = new HashMap[String, Label]
324 if not array isa JsonArray then return res
325 for obj in array do
326 if not obj isa JsonObject then continue
327 var name = obj["name"].to_s
328 res[name] = new Label.from_json(api, self, obj)
329 end
330 return res
331 end
332 # Repo default branch.
333 fun default_branch: Branch do
334 var name = json["default_branch"].to_s
335 var branch = api.load_branch(self, name)
336 assert branch isa Branch
337 return branch
338 end
339 end
340
341 # A `RepoEntity` is something contained in a `Repo`.
342 abstract class RepoEntity
343 super GithubEntity
344
345 # Repo that contains `self`.
346 var repo: Repo
347
348 # Init `self` from a `json` object.
349 init from_json(api: GithubAPI, repo: Repo, json: JsonObject) do
350 self.api = api
351 self.repo = repo
352 self.json = json
353 end
354 end
355
356 # A Github branch.
357 #
358 # Should be accessed from `GithubAPI::load_branch`.
359 #
360 # See <https://developer.github.com/v3/repos/#list-branches>.
361 class Branch
362 super RepoEntity
363
364 redef var key is lazy do return "{repo.key}/branches/{name}"
365
366 # Branch name.
367 var name: String
368
369 redef init from_json(api, repo, json) do
370 self.name = json["name"].to_s
371 super
372 end
373
374 # Get the last commit of `self`.
375 fun commit: Commit do
376 return new Commit.from_json(api, repo, json["commit"].as(JsonObject))
377 end
378
379 # List all commits in `self`.
380 #
381 # This can be long depending on the branch size.
382 # Commit are returned in an unspecified order.
383 fun commits: Array[Commit] do
384 var res = new Array[Commit]
385 var done = new HashSet[String]
386 var todos = new Array[Commit]
387 todos.add commit
388 while not todos.is_empty do
389 var commit = todos.pop
390 if done.has(commit.sha) then continue
391 done.add commit.sha
392 res.add commit
393 for parent in commit.parents do
394 todos.add parent
395 end
396 end
397 return res
398 end
399 end
400
401 # A Github commit.
402 #
403 # Should be accessed from `GithubAPI::load_commit`.
404 #
405 # See <https://developer.github.com/v3/commits/>.
406 class Commit
407 super RepoEntity
408
409 redef var key is lazy do return "{repo.key}/commits/{sha}"
410
411 # Commit SHA.
412 var sha: String
413
414 redef init from_json(api, repo, json) do
415 self.sha = json["sha"].to_s
416 super
417 end
418
419 # Parent commits of `self`.
420 fun parents: Array[Commit] do
421 var res = new Array[Commit]
422 var parents = json["parents"]
423 if not parents isa JsonArray then return res
424 for obj in parents do
425 if not obj isa JsonObject then continue
426 res.add(api.load_commit(repo, obj["sha"].to_s).as(not null))
427 end
428 return res
429 end
430
431 # Author of the commit.
432 fun author: nullable User do
433 if not json.has_key("author") then return null
434 var user = json["author"]
435 if not user isa JsonObject then return null
436 return new User.from_json(api, user)
437 end
438
439 # Committer of the commit.
440 fun committer: nullable User do
441 if not json.has_key("committer") then return null
442 var user = json["author"]
443 if not user isa JsonObject then return null
444 return new User.from_json(api, user)
445 end
446
447 # Authoring date as ISODate.
448 fun author_date: ISODate do
449 var commit = json["commit"].as(JsonObject)
450 var author = commit["author"].as(JsonObject)
451 return new ISODate.from_string(author["date"].to_s)
452 end
453
454 # Commit date as ISODate.
455 fun commit_date: ISODate do
456 var commit = json["commit"].as(JsonObject)
457 var author = commit["committer"].as(JsonObject)
458 return new ISODate.from_string(author["date"].to_s)
459 end
460
461 # Commit message.
462 fun message: String do return json["commit"].as(JsonObject)["message"].to_s
463 end
464
465 # A Github label.
466 #
467 # Should be accessed from `GithubAPI::load_label`.
468 #
469 # See <https://developer.github.com/v3/issues/labels/>.
470 class Label
471 super RepoEntity
472
473 redef var key is lazy do return "{repo.key}/labels/{name}"
474
475 # Label name.
476 var name: String
477
478 redef init from_json(api, repo, json) do
479 self.name = json["name"].to_s
480 super
481 end
482
483 # Label color code.
484 fun color: String do return json["color"].to_s
485 end