lib/github: handles milestones
[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
222 # Get the Github milestone with `id`.
223 #
224 # Returns `null` if the milestone cannot be found.
225 #
226 # var api = new GithubAPI(get_github_oauth)
227 # var repo = api.load_repo("privat/nit")
228 # assert repo isa Repo
229 # var stone = api.load_milestone(repo, 4)
230 # assert stone.title == "v1.0prealpha"
231 fun load_milestone(repo: Repo, id: Int): nullable Milestone do
232 var milestone = new Milestone(self, repo, id)
233 milestone.load_from_github
234 if was_error then return null
235 return milestone
236 end
237 end
238
239 # Something returned by the Github API.
240 #
241 # Mainly a Nit wrapper around a JSON objet.
242 abstract class GithubEntity
243
244 # Github API instance.
245 var api: GithubAPI
246
247 # FIXME constructor should be private
248
249 # Key used to access this entity from Github api base.
250 fun key: String is abstract
251
252 # JSON representation of `self`.
253 #
254 # This is the same json structure than used by Github API.
255 var json: JsonObject is noinit, protected writable
256
257 # Load `json` from Github API.
258 private fun load_from_github do
259 json = api.load_from_github(key)
260 end
261
262 redef fun to_s do return json.to_json
263 end
264
265 # A Github user.
266 #
267 # Should be accessed from `GithubAPI::load_user`.
268 #
269 # See <https://developer.github.com/v3/users/>.
270 class User
271 super GithubEntity
272
273 redef var key is lazy do return "users/{login}"
274
275 # Github login.
276 var login: String
277
278 # Init `self` from a `json` object.
279 init from_json(api: GithubAPI, json: JsonObject) do
280 init(api, json["login"].to_s)
281 self.json = json
282 end
283
284 # Github User page url.
285 fun html_url: String do return json["html_url"].to_s
286
287 # Avatar image url for this user.
288 fun avatar_url: String do return json["avatar_url"].to_s
289 end
290
291 # A Github repository.
292 #
293 # Should be accessed from `GithubAPI::load_repo`.
294 #
295 # See <https://developer.github.com/v3/repos/>.
296 class Repo
297 super GithubEntity
298
299 redef var key is lazy do return "repos/{full_name}"
300
301 # Repo full name on Github.
302 var full_name: String
303
304 # Init `self` from a `json` object.
305 init from_json(api: GithubAPI, json: JsonObject) do
306 init(api, json["full_name"].to_s)
307 self.json = json
308 end
309
310 # Repo short name on Github.
311 fun name: String do return json["name"].to_s
312
313 # Github User page url.
314 fun html_url: String do return json["html_url"].to_s
315
316 # Get the repo owner.
317 fun owner: User do
318 return new User.from_json(api, json["owner"].as(JsonObject))
319 end
320
321 # List of branches associated with their names.
322 fun branches: Map[String, Branch] do
323 api.message(1, "Get branches for {full_name}")
324 var array = api.get("repos/{full_name}/branches")
325 var res = new HashMap[String, Branch]
326 if not array isa JsonArray then return res
327 for obj in array do
328 if not obj isa JsonObject then continue
329 var name = obj["name"].to_s
330 res[name] = new Branch.from_json(api, self, obj)
331 end
332 return res
333 end
334
335 # List of labels associated with their names.
336 fun labels: Map[String, Label] do
337 api.message(1, "Get labels for {full_name}")
338 var array = api.get("repos/{full_name}/labels")
339 var res = new HashMap[String, Label]
340 if not array isa JsonArray then return res
341 for obj in array do
342 if not obj isa JsonObject then continue
343 var name = obj["name"].to_s
344 res[name] = new Label.from_json(api, self, obj)
345 end
346 return res
347 end
348
349 # List of milestones associated with their ids.
350 fun milestones: Map[Int, Milestone] do
351 api.message(1, "Get milestones for {full_name}")
352 var array = api.get("repos/{full_name}/milestones")
353 var res = new HashMap[Int, Milestone]
354 if array isa JsonArray then
355 for obj in array do
356 if not obj isa JsonObject then continue
357 var number = obj["number"].as(Int)
358 res[number] = new Milestone.from_json(api, self, obj)
359 end
360 end
361 return res
362 end
363
364 # Repo default branch.
365 fun default_branch: Branch do
366 var name = json["default_branch"].to_s
367 var branch = api.load_branch(self, name)
368 assert branch isa Branch
369 return branch
370 end
371 end
372
373 # A `RepoEntity` is something contained in a `Repo`.
374 abstract class RepoEntity
375 super GithubEntity
376
377 # Repo that contains `self`.
378 var repo: Repo
379
380 # Init `self` from a `json` object.
381 init from_json(api: GithubAPI, repo: Repo, json: JsonObject) do
382 self.api = api
383 self.repo = repo
384 self.json = json
385 end
386 end
387
388 # A Github branch.
389 #
390 # Should be accessed from `GithubAPI::load_branch`.
391 #
392 # See <https://developer.github.com/v3/repos/#list-branches>.
393 class Branch
394 super RepoEntity
395
396 redef var key is lazy do return "{repo.key}/branches/{name}"
397
398 # Branch name.
399 var name: String
400
401 redef init from_json(api, repo, json) do
402 self.name = json["name"].to_s
403 super
404 end
405
406 # Get the last commit of `self`.
407 fun commit: Commit do
408 return new Commit.from_json(api, repo, json["commit"].as(JsonObject))
409 end
410
411 # List all commits in `self`.
412 #
413 # This can be long depending on the branch size.
414 # Commit are returned in an unspecified order.
415 fun commits: Array[Commit] do
416 var res = new Array[Commit]
417 var done = new HashSet[String]
418 var todos = new Array[Commit]
419 todos.add commit
420 while not todos.is_empty do
421 var commit = todos.pop
422 if done.has(commit.sha) then continue
423 done.add commit.sha
424 res.add commit
425 for parent in commit.parents do
426 todos.add parent
427 end
428 end
429 return res
430 end
431 end
432
433 # A Github commit.
434 #
435 # Should be accessed from `GithubAPI::load_commit`.
436 #
437 # See <https://developer.github.com/v3/commits/>.
438 class Commit
439 super RepoEntity
440
441 redef var key is lazy do return "{repo.key}/commits/{sha}"
442
443 # Commit SHA.
444 var sha: String
445
446 redef init from_json(api, repo, json) do
447 self.sha = json["sha"].to_s
448 super
449 end
450
451 # Parent commits of `self`.
452 fun parents: Array[Commit] do
453 var res = new Array[Commit]
454 var parents = json["parents"]
455 if not parents isa JsonArray then return res
456 for obj in parents do
457 if not obj isa JsonObject then continue
458 res.add(api.load_commit(repo, obj["sha"].to_s).as(not null))
459 end
460 return res
461 end
462
463 # Author of the commit.
464 fun author: nullable User do
465 if not json.has_key("author") then return null
466 var user = json["author"]
467 if not user isa JsonObject then return null
468 return new User.from_json(api, user)
469 end
470
471 # Committer of the commit.
472 fun committer: nullable User do
473 if not json.has_key("committer") then return null
474 var user = json["author"]
475 if not user isa JsonObject then return null
476 return new User.from_json(api, user)
477 end
478
479 # Authoring date as ISODate.
480 fun author_date: ISODate do
481 var commit = json["commit"].as(JsonObject)
482 var author = commit["author"].as(JsonObject)
483 return new ISODate.from_string(author["date"].to_s)
484 end
485
486 # Commit date as ISODate.
487 fun commit_date: ISODate do
488 var commit = json["commit"].as(JsonObject)
489 var author = commit["committer"].as(JsonObject)
490 return new ISODate.from_string(author["date"].to_s)
491 end
492
493 # Commit message.
494 fun message: String do return json["commit"].as(JsonObject)["message"].to_s
495 end
496
497 # A Github label.
498 #
499 # Should be accessed from `GithubAPI::load_label`.
500 #
501 # See <https://developer.github.com/v3/issues/labels/>.
502 class Label
503 super RepoEntity
504
505 redef var key is lazy do return "{repo.key}/labels/{name}"
506
507 # Label name.
508 var name: String
509
510 redef init from_json(api, repo, json) do
511 self.name = json["name"].to_s
512 super
513 end
514
515 # Label color code.
516 fun color: String do return json["color"].to_s
517 end
518
519 # A Github milestone.
520 #
521 # Should be accessed from `GithubAPI::load_milestone`.
522 #
523 # See <https://developer.github.com/v3/issues/milestones/>.
524 class Milestone
525 super RepoEntity
526
527 redef var key is lazy do return "{repo.key}/milestones/{number}"
528
529 # The milestone id on Github.
530 var number: Int
531
532 redef init from_json(api, repo, json) do
533 super
534 self.number = json["number"].as(Int)
535 end
536
537 # Milestone title.
538 fun title: String do return json["title"].to_s
539
540 # Milestone long description.
541 fun description: String do return json["description"].to_s
542
543 # Count of opened issues linked to this milestone.
544 fun open_issues: Int do return json["open_issues"].to_s.to_i
545
546 # Count of closed issues linked to this milestone.
547 fun closed_issues: Int do return json["closed_issues"].to_s.to_i
548
549 # Milestone state.
550 fun state: String do return json["state"].to_s
551
552 # Creation time in ISODate format.
553 fun created_at: ISODate do
554 return new ISODate.from_string(json["created_at"].to_s)
555 end
556
557 # User that created this milestone.
558 fun creator: User do
559 return new User.from_json(api, json["creator"].as(JsonObject))
560 end
561
562 # Due time in ISODate format (if any).
563 fun due_on: nullable ISODate do
564 var res = json["updated_at"]
565 if res == null then return null
566 return new ISODate.from_string(res.to_s)
567 end
568
569 # Update time in ISODate format (if any).
570 fun updated_at: nullable ISODate do
571 var res = json["updated_at"]
572 if res == null then return null
573 return new ISODate.from_string(res.to_s)
574 end
575
576 # Close time in ISODate format (if any).
577 fun closed_at: nullable ISODate do
578 var res = json["closed_at"]
579 if res == null then return null
580 return new ISODate.from_string(res.to_s)
581 end
582 end